diff --git a/Dockerfile b/Dockerfile index 9790e61..3ff97c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM fedora RUN dnf -y install --setopt=install_weak_deps=false --setopt=tsflags=nodocs \ --setopt=deltarpm=false python2-rpm libtaskotron-core libtaskotron-fedora \ python3-rpm tox python2 python3 python2-dnf python3-dnf \ - python2-libarchive-c && dnf clean all + python2-libarchive-c python-bugzilla && dnf clean all ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 diff --git a/README.rst b/README.rst index 137631d..bf8d013 100644 --- a/README.rst +++ b/README.rst @@ -13,9 +13,12 @@ Currently the following checks are available: - Whether the package uses versioned Python prefix in requirements' names; -- Whether only Python 2 version of the package contains the executables. +- Whether only Python 2 version of the package contains the executables; + +- Whether the package uses versioned shebangs in its executables; + +- Whether the package supports Python 3 upstream but not in the package. -- Whether the package uses versioned shebangs in its executables. Running ------- diff --git a/python_versions_check.py b/python_versions_check.py index d2c7307..deaa92a 100644 --- a/python_versions_check.py +++ b/python_versions_check.py @@ -19,6 +19,7 @@ task_requires_naming_scheme, task_executables, task_unversioned_shebangs, + task_py3_support, ) from taskotron_python_versions.common import log, Package, PackageException @@ -59,6 +60,8 @@ def run(koji_build, workdir='.', artifactsdir='artifacts'): srpm_packages + packages, koji_build, artifact)) details.append(task_executables(packages, koji_build, artifact)) details.append(task_unversioned_shebangs(packages, koji_build, artifact)) + details.append(task_py3_support( + srpm_packages + packages, koji_build, artifact)) # finally, the main detail with overall results outcome = 'PASSED' diff --git a/runtask.yml b/runtask.yml index b694db9..23cb487 100644 --- a/runtask.yml +++ b/runtask.yml @@ -15,6 +15,7 @@ environment: - rpm-python - python2-dnf - python2-libarchive-c + - python-bugzilla actions: - name: download rpms from koji diff --git a/setup.py b/setup.py index 44fdb67..58f76d1 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ url='https://github.com/fedora-python/taskotron-python-versions', license='Public Domain', packages=find_packages(), - install_requires=['libarchive-c'], + install_requires=['libarchive-c', 'bugzilla'], setup_requires=['setuptools', 'pytest-runner'], tests_require=['pytest', 'pyyaml'], classifiers=[ diff --git a/taskotron_python_versions/__init__.py b/taskotron_python_versions/__init__.py index 80c452b..6bb7633 100644 --- a/taskotron_python_versions/__init__.py +++ b/taskotron_python_versions/__init__.py @@ -3,6 +3,7 @@ from .requires import task_requires_naming_scheme from .two_three import task_two_three from .unversioned_shebangs import task_unversioned_shebangs +from .py3_support import task_py3_support __all__ = ( @@ -11,4 +12,5 @@ 'task_requires_naming_scheme', 'task_executables', 'task_unversioned_shebangs', + 'task_py3_support', ) diff --git a/taskotron_python_versions/common.py b/taskotron_python_versions/common.py index d83b334..924545d 100644 --- a/taskotron_python_versions/common.py +++ b/taskotron_python_versions/common.py @@ -1,3 +1,4 @@ +import collections import logging import os @@ -32,6 +33,19 @@ def write_to_artifact(artifact, message, info_url): bug_url=BUG_URL)) +def packages_by_version(packages): + """Given the list of packages, group them by the Python + version they are built for. + + Return: (dict) Python version: list of packages + """ + pkg_by_version = collections.defaultdict(list) + for package in packages: + for version in package.py_versions: + pkg_by_version[version].append(package) + return pkg_by_version + + class PackageException(Exception): """Base Exception class for Package API.""" diff --git a/taskotron_python_versions/executables.py b/taskotron_python_versions/executables.py index 572dc78..6698536 100644 --- a/taskotron_python_versions/executables.py +++ b/taskotron_python_versions/executables.py @@ -1,6 +1,6 @@ import collections -from .common import log, write_to_artifact +from .common import log, write_to_artifact, packages_by_version INFO_URL = ('https://fedoraproject.org/wiki/Packaging:Python#' @@ -66,11 +66,7 @@ def task_executables(packages, koji_build, artifact): outcome = 'PASSED' message = '' - pkg_by_version = collections.defaultdict(list) - for package in packages: - for version in package.py_versions: - pkg_by_version[version].append(package) - + pkg_by_version = packages_by_version(packages) py2_packages = pkg_by_version[2] py3_packages = pkg_by_version[3] diff --git a/taskotron_python_versions/py3_support.py b/taskotron_python_versions/py3_support.py new file mode 100644 index 0000000..665aee0 --- /dev/null +++ b/taskotron_python_versions/py3_support.py @@ -0,0 +1,124 @@ +import bugzilla + +from .common import log, write_to_artifact, packages_by_version + + +INFO_URL = 'https://fedoraproject.org/wiki/Packaging:Python' + +MESSAGE = """ +This software supports Python 3 upstream, but is not +packaged for Python 3 in Fedora. + +Software MUST be packaged for Python 3 if upstream supports it. +See the following Bugzilla: +{} +""" + +BUGZILLA_URL = "bugzilla.redhat.com" +PY3_TRACKER_BUG = 1285816 +# Bugzilla trackers, for which taskotron checks already exist. +IGNORE_TRACKER_BUGS = [ + 1432186, # Missing PY3-EXECUTABLES + 1340802, # Depends on both Py2 and Py3 +] +IGNORE_STATUSES = [ + 'CLOSED', + 'VERIFIED', + 'RELEASE_PENDING', + 'ON_QA', +] + + +def ignored(bug): + """Check if the Bugzilla should be ignored. + + Reasons to ignore a bug: + - tracked by any of IGNORE_TRACKER_BUGS, so there is a + separate check for it; + - status is one of IGNORE_STATUSES, so the package is most + probably ported in rawhide. + + Return: (bool) True if bug should be ignored, False otherwise + """ + for tracker in bug.blocks: + if tracker in IGNORE_TRACKER_BUGS: + return True + return bug.status in IGNORE_STATUSES + + +def filter_urls(bugs): + """Given the list of bugs, return the list of URLs + for those which should not be ignored. + + Return: (list of str) List of links + """ + return [bug.weburl for bug in bugs if not ignored(bug)] + + +def get_py3_bugzillas_for(srpm_name): + """Fetch all Bugzillas for the package given it's SRPM name, + which are tracked by PY3_TRACKER_BUG. + + Return: (list) List of Bugzilla URLs + """ + bzapi = bugzilla.Bugzilla(BUGZILLA_URL) + query = bzapi.build_query( + product="Fedora", + component=srpm_name) + query['blocks'] = PY3_TRACKER_BUG + bugs = bzapi.query(query) + return filter_urls(bugs) + + +def ported_to_py3(packages): + """Check if the package is ported to Python 3, + by comparing the number of it's binary RPMs for each + Python version. + + Return: (bool) True if ported, False otherwise + """ + pkg_by_version = packages_by_version(packages) + return len(pkg_by_version[2]) <= len(pkg_by_version[3]) + + +def task_py3_support(packages, koji_build, artifact): + """Check that the package is packaged for Python 3, + if upstream is Python 3 ready. + + Source of data: https://bugzilla.redhat.com/show_bug.cgi?id=1285816 + """ + # libtaskotron is not available on Python 3, so we do it inside + # to make the above functions testable anyway + from libtaskotron import check + + outcome = 'PASSED' + message = '' + + srpm, packages = packages[0], packages[1:] + if not ported_to_py3(packages): + bugzilla_urls = get_py3_bugzillas_for(srpm.name) + if bugzilla_urls: + outcome = 'FAILED' + log.error( + 'This software supports Python 3 upstream,' + ' but is not packaged for Python 3 in Fedora') + message = ', '.join(bugzilla_urls) + else: + log.info( + 'This software does not support Python 3' + ' upstream, skipping Py3 support check') + + detail = check.CheckDetail( + checkname='python-versions.py3_support', + item=koji_build, + report_type=check.ReportType.KOJI_BUILD, + outcome=outcome) + + if message: + detail.artifact = artifact + write_to_artifact(artifact, MESSAGE.format(message), INFO_URL) + + log.info('python-versions.py3_support {} for {}'.format( + outcome, koji_build)) + + return detail diff --git a/test/functional/test_py3_support.py b/test/functional/test_py3_support.py new file mode 100644 index 0000000..e9fb59d --- /dev/null +++ b/test/functional/test_py3_support.py @@ -0,0 +1,66 @@ +from collections import namedtuple + +import pytest + +from taskotron_python_versions.py3_support import ( + ignored, + filter_urls, + ported_to_py3, + PY3_TRACKER_BUG, + IGNORE_TRACKER_BUGS, +) +from taskotron_python_versions.two_three import check_two_three +from .common import gpkg + + +BugStub = namedtuple('BugStub', 'blocks, weburl, status') +BugStub.__new__.__defaults__ = (None, 'http://test', 'NEW') + + +@pytest.mark.parametrize('bug', ( + BugStub(blocks=IGNORE_TRACKER_BUGS), + BugStub(blocks=[PY3_TRACKER_BUG] + IGNORE_TRACKER_BUGS), + BugStub(blocks=IGNORE_TRACKER_BUGS + ['X']), + BugStub(blocks=IGNORE_TRACKER_BUGS, status='NEW'), + BugStub(blocks=[PY3_TRACKER_BUG], status='CLOSED'), + BugStub(blocks=[PY3_TRACKER_BUG], status='VERIFIED'), + BugStub(blocks=[PY3_TRACKER_BUG], status='RELEASE_PENDING'), + BugStub(blocks=[PY3_TRACKER_BUG], status='ON_QA'), +)) +def test_ignored(bug): + assert ignored(bug) + + +@pytest.mark.parametrize('bug', ( + BugStub(blocks=[PY3_TRACKER_BUG]), + BugStub(blocks=[PY3_TRACKER_BUG, 'X']), + BugStub(blocks=[PY3_TRACKER_BUG], status='NEW'), + BugStub(blocks=[PY3_TRACKER_BUG], status='ASSIGNED'), +)) +def test_not_ignored(bug): + assert not ignored(bug) + + +@pytest.mark.parametrize(('bugs', 'expected'), ( + ([BugStub(blocks=[PY3_TRACKER_BUG])], ['http://test']), + ([BugStub(blocks=[PY3_TRACKER_BUG], status='NEW')], ['http://test']), + ([BugStub(blocks=[PY3_TRACKER_BUG], status='CLOSED')], []), + ([BugStub(blocks=IGNORE_TRACKER_BUGS)], []), + ([BugStub(blocks=IGNORE_TRACKER_BUGS, status='NEW')], []), + ([BugStub(blocks=[PY3_TRACKER_BUG], weburl='test1'), + BugStub(blocks=IGNORE_TRACKER_BUGS, weburl='test2')], ['test1']), +)) +def test_filter_urls(bugs, expected): + assert filter_urls(bugs) == expected + + +@pytest.mark.parametrize(('pkgglobs', 'expected'), ( + (('pyserial*', 'python3-pyserial*'), True), + (('pyserial*',), False), + (('python3-pyserial*',), True), +)) +def test_ported_to_py3(pkgglobs, expected): + packages = [gpkg(pkg) for pkg in pkgglobs] + for package in packages: + check_two_three(package) + assert ported_to_py3(packages) == expected diff --git a/test/integration/test_integration.py b/test/integration/test_integration.py index e725991..9c3ba2d 100644 --- a/test/integration/test_integration.py +++ b/test/integration/test_integration.py @@ -116,21 +116,26 @@ def results(request): _nodejs = fixtures_factory('nodejs-semver-5.1.1-2.fc26') nodejs = fixtures_factory('_nodejs') +_bucky = fixtures_factory('python-bucky-2.2.2-7.fc27') +bucky = fixtures_factory('_bucky') + @pytest.mark.parametrize('results', ('eric', 'six', 'admesh', 'tracer', 'copr', 'epub', 'twine', 'yum', - 'vdirsyncer', 'docutils', 'nodejs')) + 'vdirsyncer', 'docutils', 'nodejs', + 'bucky')) def test_number_of_results(results, request): # getting a fixture by name # https://github.com/pytest-dev/pytest/issues/349#issuecomment-112203541 results = request.getfixturevalue(results) # Each time a new check is added, this number needs to be increased - assert len(results) == 6 + assert len(results) == 7 @pytest.mark.parametrize('results', ('eric', 'six', 'admesh', - 'copr', 'epub', 'twine')) + 'copr', 'epub', 'twine', + 'bucky')) def test_two_three_passed(results, request): results = request.getfixturevalue(results) assert results['python-versions.two_three'].outcome == 'PASSED' @@ -175,7 +180,7 @@ def test_naming_scheme_passed(results, request): assert results['python-versions.naming_scheme'].outcome == 'PASSED' -@pytest.mark.parametrize('results', ('copr', 'six', 'admesh')) +@pytest.mark.parametrize('results', ('copr', 'six', 'admesh', 'bucky')) def test_naming_scheme_failed(results, request): results = request.getfixturevalue(results) assert results['python-versions.naming_scheme'].outcome == 'FAILED' @@ -240,7 +245,7 @@ def test_requires_naming_scheme_contains_python(yum): @pytest.mark.parametrize('results', ('eric', 'six', 'admesh', 'tracer', - 'copr', 'epub', 'twine')) + 'copr', 'epub', 'twine', 'bucky')) def test_executables_passed(results, request): results = request.getfixturevalue(results) task_result = results['python-versions.executables'] @@ -292,7 +297,7 @@ def test_unvesioned_shebangs_passed(results, request): assert results['python-versions.unversioned_shebangs'].outcome == 'PASSED' -@pytest.mark.parametrize('results', ('yum', 'tracer')) +@pytest.mark.parametrize('results', ('yum', 'tracer', 'bucky')) def test_unvesioned_shebangs_failed(results, request): results = request.getfixturevalue(results) assert results['python-versions.unversioned_shebangs'].outcome == 'FAILED' @@ -314,3 +319,42 @@ def test_artifact_contains_unversioned_shebangs_and_looks_as_expected( This is discouraged and should be avoided. Please check the shebangs and use either `#!/usr/bin/python2` or `#!/usr/bin/python3`. """).strip() in artifact.strip() + + +@pytest.mark.parametrize('results', ('eric', 'six', 'admesh', 'tracer', + 'copr', 'epub', 'twine', 'docutils')) +def test_py3_support_passed(results, request): + results = request.getfixturevalue(results) + task_result = results['python-versions.py3_support'] + assert task_result.outcome == 'PASSED' + + +@pytest.mark.parametrize('results', ('bucky',)) +def test_py3_support_failed(results, request): + results = request.getfixturevalue(results) + task_result = results['python-versions.py3_support'] + assert task_result.outcome == 'FAILED' + + +def test_artifact_contains_py3_support_and_looks_as_expected( + bucky): + """Test that py3_support check fails if the package is mispackaged. + + NOTE: The test will start to fail as soon as python-bucky + gets ported to Python 3 and its Bugzilla gets closed. + See https://bugzilla.redhat.com/show_bug.cgi?id=1367012 + """ + result = bucky['python-versions.py3_support'] + with open(result.artifact) as f: + artifact = f.read() + + print(artifact) + + assert dedent(""" + This software supports Python 3 upstream, but is not + packaged for Python 3 in Fedora. + + Software MUST be packaged for Python 3 if upstream supports it. + See the following Bugzilla: + https://bugzilla.redhat.com/show_bug.cgi?id=1367012 + """).strip() in artifact.strip() diff --git a/tox.ini b/tox.ini index dcf787b..5a1213a 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ skipsdist = True deps = pytest libarchive-c + bugzilla commands = python -m pytest -v {posargs} test/functional sitepackages = True