diff --git a/README.rst b/README.rst index 0511535..5c2b8ca 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,23 @@ You can specify jira issue ID in docstring or in pytest.mark.jira decorator. If you use decorator you can specify optional parameter ``run``. If it's false and issue is unresolved, the test will be skipped. +You can disable searching for issue ID in doc string by using +``--jira-disable-docs-search`` parameter or by ``docs_search=False`` +in `jira.cfg`. It's also possible to change behavior if issue ID was not found +by setting ``--jira-marker-strategy=STRATEGY`` or in config file +as `marker_strategy=STRATEGY`. + +You can use one of following strategies: + +- open - issue is considered as open (default) +- strict - raise an exception +- ignore - issue id is ignored +- warn - write error message and ignore + +By default the regular expression patter for matching jira issue ID is `[A-Z]+-[0-9]+`, +it can by changed by ``--jira-issue-regex=REGEX`` or in a config file by +``jira_regex=REGEX``. + Example ^^^^^^^ .. code:: python @@ -88,6 +105,9 @@ Usage # ssl_verification = True/False # version = foo-1.0 # components = com1,second component,com3 + # strategy = [open|strict|warn|ignore] (dealing with not found issues) + # docs_search = False (disable searching for issue id in docs) + # issue_regex = REGEX (replace default `[A-Z]+-[0-9]+` regular expression) Options can be overridden with command line options. The configuration file can also be placed in ``/etc/jira.cfg`` and ``~/jira.cfg``. diff --git a/pytest_jira.py b/pytest_jira.py index a8e42fd..594d58c 100644 --- a/pytest_jira.py +++ b/pytest_jira.py @@ -12,54 +12,26 @@ import re import six import pytest +import sys from jira.client import JIRA class JiraHooks(object): - issue_re = r"([A-Z]+-[0-9]+)" - def __init__( self, connection, + marker, version=None, components=None, ): self.conn = connection + self.mark = marker self.components = set(components) if components else None self.version = version # Speed up JIRA lookups for duplicate issues self.issue_cache = dict() - def get_jira_issues(self, item): - issue_pattern = re.compile(self.issue_re) - jira_ids = [] - # Was the jira marker used? - if 'jira' in item.keywords: - marker = item.keywords['jira'] - if len(marker.args) == 0: - raise TypeError('JIRA marker requires one, or more, arguments') - jira_ids.extend(item.keywords['jira'].args) - - # Was a jira issue referenced in the docstr? - if item.function.__doc__: - jira_ids.extend( - [ - m.group(0) - for m in issue_pattern.finditer(item.function.__doc__) - ] - ) - - # Filter valid issues, and return unique issues - for jid in set(jira_ids): - if not issue_pattern.match(jid): - raise ValueError( - 'JIRA marker argument `%s` does not match pattern' % jid - ) - return list( - set(jira_ids) - ) - def is_issue_resolved(self, issue_id): ''' Returns whether the provided issue ID is resolved (True|False). Will @@ -70,9 +42,12 @@ def is_issue_resolved(self, issue_id): try: self.issue_cache[issue_id] = self.conn.get_issue(issue_id) except Exception: - self.issue_cache[issue_id] = {'status': 'open'} + self.issue_cache[issue_id] = self.mark.get_default(issue_id) # Skip test if issue remains unresolved + if self.issue_cache[issue_id] is None: + return True + if self.issue_cache[issue_id]['status'] in ['closed', 'resolved']: return self.fixed_in_version(issue_id) else: @@ -86,7 +61,7 @@ def pytest_runtest_makereport(self, item, call, __multicall__): rep = __multicall__.execute() try: - jira_ids = self.get_jira_issues(item) + jira_ids = self.mark.get_jira_issues(item) except Exception: jira_ids = [] @@ -112,7 +87,7 @@ def pytest_runtest_setup(self, item): jira_run = True if 'jira' in item.keywords: jira_run = item.keywords['jira'].kwargs.get('run', jira_run) - jira_ids = self.get_jira_issues(item) + jira_ids = self.mark.get_jira_issues(item) # Check all linked issues for issue_id in jira_ids: @@ -201,12 +176,68 @@ def get_url(self): return self.url +class JiraMarkerReporter(object): + issue_re = r"([A-Z]+-[0-9]+)" + + def __init__(self, strategy, docs, patern): + self.issue_pattern = re.compile(patern or self.issue_re) + self.docs = docs + self.strategy = strategy.lower() + + def get_jira_issues(self, item): + jira_ids = [] + # Was the jira marker used? + if 'jira' in item.keywords: + marker = item.keywords['jira'] + if len(marker.args) == 0: + raise TypeError('JIRA marker requires one, or more, arguments') + jira_ids.extend(item.keywords['jira'].args) + + # Was a jira issue referenced in the docstr? + if self.docs and item.function.__doc__: + jira_ids.extend( + [ + m.group(0) + for m in self.issue_pattern.finditer(item.function.__doc__) + ] + ) + + # Filter valid issues, and return unique issues + for jid in set(jira_ids): + if not self.issue_pattern.match(jid): + raise ValueError( + 'JIRA marker argument `%s` does not match pattern' % jid + ) + return list( + set(jira_ids) + ) + + def get_default(self, jid): + if self.strategy == 'open': + return {'status': 'open'} + if self.strategy == 'strict': + raise ValueError( + 'JIRA marker argument `%s` was not found' % jid + ) + if self.strategy == 'warn': + sys.stderr.write( + 'JIRA marker argument `%s` was not found' % jid + ) + return None + + def _get_value(config, section, name, default=None): if config.has_option(section, name): return config.get(section, name) return default +def _get_bool(config, section, name, default=False): + if config.has_option(section, name): + return config.getboolean(section, name) + return default + + def pytest_addoption(parser): """ Add a options section to py.test --help for jira integration. @@ -232,11 +263,6 @@ def pytest_addoption(parser): ] ) - try: - verify = config.getboolean('DEFAULT', 'ssl_verification') - except six.moves.configparser.NoOptionError: - verify = True - group.addoption('--jira-url', action='store', dest='jira_url', @@ -258,8 +284,10 @@ def pytest_addoption(parser): group.addoption('--jira-no-ssl-verify', action='store_false', dest='jira_verify', - default=verify, - help='Disable SSL verification to Jira' + default=_get_bool( + config, 'DEFAULT', 'ssl_verification', True, + ), + help='Disable SSL verification to Jira', ) group.addoption('--jira-components', action='store', @@ -274,6 +302,32 @@ def pytest_addoption(parser): default=_get_value(config, 'DEFAULT', 'version'), help='Used version' ) + group.addoption('--jira-marker-strategy', + action='store', + dest='jira_marker_strategy', + default=_get_value( + config, 'DEFAULT', 'marker_strategy', 'open' + ), + choices=['open', 'strict', 'ignore', 'warn'], + help='''Action if issue ID was not found + open - issue is considered as open (default) + strict - raise an exception + ignore - issue id is ignored + warn - write error message and ignore + ''', + ) + group.addoption('--jira-disable-docs-search', + action='store_false', + dest='jira_docs', + default=_get_bool(config, 'DEFAULT', 'docs_search', True), + help='Issue ID in doc strings will be ignored' + ) + group.addoption('--jira-issue-regex', + action='store', + dest='jira_regex', + default=_get_value(config, 'DEFAULT', 'issue_regex'), + help='Replace default `[A-Z]+-[0-9]+` regular expression' + ) def pytest_configure(config): @@ -302,10 +356,16 @@ def pytest_configure(config): config.getvalue('jira_password'), config.getvalue('jira_verify'), ) + jira_marker = JiraMarkerReporter( + config.getvalue('jira_marker_strategy'), + config.getvalue('jira_docs'), + config.getvalue('jira_regex'), + ) if jira_connection.is_connected(): # if connection to jira fails, plugin won't be loaded jira_plugin = JiraHooks( jira_connection, + jira_marker, config.getvalue('jira_product_version'), components, ) diff --git a/tests/test_jira.py b/tests/test_jira.py index 3ba29b8..cbe7b1c 100644 --- a/tests/test_jira.py +++ b/tests/test_jira.py @@ -410,3 +410,144 @@ def test_pass(): 'com1', ) assert_outcomes(result, 0, 1, 0) + + +def test_strategy_ignore_failed(testdir): + '''Invalid issue ID is ignored and test failes''' + testdir.makeconftest(CONFTEST) + testdir.makefile( + '.cfg', + jira="\n".join([ + '[DEFAULT]', + 'url = https://issues.jboss.org', + 'marker_strategy = ignore', + 'docs_search = False', + ]) + ) + testdir.makepyfile(""" + import pytest + + @pytest.mark.jira("ORG-1412789456148865", run=True) + def test_fail(): + assert False + """) + result = testdir.runpytest('--jira') + result.assert_outcomes(0, 0, 1) + + +def test_strategy_strict_exception(testdir): + '''Invalid issue ID, exception is rised''' + testdir.makeconftest(CONFTEST) + testdir.makepyfile(""" + import pytest + + def test_fail(): + \"\"\" + issue: 89745-1412789456148865 + \"\"\" + assert False + """) + result = testdir.runpytest( + '--jira', + '--jira-url', 'https://issues.jboss.org', + '--jira-marker-strategy', 'strict', + '--jira-issue-regex', '[0-9]+-[0-9]+', + ) + assert "89745-1412789456148865" in result.stdout.str() + + +def test_strategy_warn_fail(testdir): + '''Invalid issue ID is ignored and warning is written''' + testdir.makeconftest(CONFTEST) + testdir.makefile( + '.cfg', + jira="\n".join([ + '[DEFAULT]', + 'url = https://issues.jboss.org', + 'marker_strategy = warn', + ]) + ) + testdir.makepyfile(""" + import pytest + + @pytest.mark.jira("ORG-1511786754387", run=True) + def test_fail(): + assert False + """) + result = testdir.runpytest('--jira') + assert "ORG-1511786754387" in result.stderr.str() + result.assert_outcomes(0, 0, 1) + + +def test_ignored_docs_marker_fail(testdir): + '''Issue is open but docs is ignored''' + testdir.makeconftest(CONFTEST) + testdir.makepyfile(""" + import pytest + + def test_fail(): + \"\"\" + open issue: ORG-1382 + ignored + \"\"\" + assert False + """) + result = testdir.runpytest( + '--jira', + '--jira-url', 'https://issues.jboss.org', + '--jira-disable-docs-search', + ) + assert_outcomes(result, 0, 0, 1) + + +def test_issue_not_found_considered_open_xfailed(testdir): + '''Issue is open but docs is ignored''' + testdir.makeconftest(CONFTEST) + testdir.makepyfile(""" + import pytest + + def test_fail(): + \"\"\" + not existing issue: ORG-13827864532876523 + considered open by default + \"\"\" + assert False + """) + result = testdir.runpytest(*PLUGIN_ARGS) + assert_outcomes(result, 0, 0, 0, xfailed=1) + + +def test_jira_marker_bad_args_due_to_changed_regex(testdir): + '''Issue ID in marker doesn't match due to changed regex''' + testdir.makeconftest(CONFTEST) + testdir.makepyfile(""" + import pytest + + @pytest.mark.jira("ORG-1382", run=False) + def test_fail(): + assert False + """) + result = testdir.runpytest( + '--jira', + '--jira-url', 'https://issues.jboss.org', + '--jira-issue-regex', '[0-9]+-[0-9]+', + ) + assert_outcomes(result, 0, 0, 0, error=1) + + +def test_invalid_jira_marker_strategy_parameter(testdir): + '''Invalid parameter for --jira-marker-strategy''' + testdir.makeconftest(CONFTEST) + testdir.makepyfile(""" + import pytest + + @pytest.mark.jira("ORG-1382", run=False) + def test_fail(): + assert False + """) + result = testdir.runpytest( + '--jira', + '--jira-url', 'https://issues.jboss.org', + '--jira-marker-strategy', 'invalid', + ) + assert "invalid choice: \'invalid\'" in result.stderr.str()