From 0d1453641ae67461954e73ad15d6c17b24f20495 Mon Sep 17 00:00:00 2001 From: Kevin James Date: Mon, 30 Dec 2019 17:23:42 -0600 Subject: [PATCH 1/5] tests: include coverage v5 in matrix --- setup.py | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 832a1367..63ff1602 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ ], }, install_requires=[ - 'coverage>=3.6,<5.0', + 'coverage>=3.6,<6.0', 'docopt>=0.6.1', 'requests>=1.0.0', ], diff --git a/tox.ini b/tox.ini index c804669b..dd5ed0d1 100644 --- a/tox.ini +++ b/tox.ini @@ -20,13 +20,13 @@ deps = cov3: coverage<4.0 cov4: coverage>=4.0,<4.1 cov41: coverage>=4.1,<5.0 - cov5: coverage==5.0b1 + cov5: coverage>=5.0,<6.0 commands = coverage run --branch --source=coveralls -m pytest tests/ coverage report -m [testenv:coveralls] deps = - coverage<5.0 + coverage<6.0 commands = coveralls --verbose From 77a3d000e090155bd913cbd60d98ee35f42ae76d Mon Sep 17 00:00:00 2001 From: Kevin James Date: Mon, 30 Dec 2019 17:48:14 -0600 Subject: [PATCH 2/5] refactor: compose coverage Reporter instead of inherit --- coveralls/reporter.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/coveralls/reporter.py b/coveralls/reporter.py index 8f27550b..57aa4cfd 100644 --- a/coveralls/reporter.py +++ b/coveralls/reporter.py @@ -15,12 +15,12 @@ log = logging.getLogger('coveralls.reporter') -class CoverallReporter(Reporter): +class CoverallReporter(object): """Custom coverage.py reporter for coveralls.io""" def __init__(self, *args, **kwargs): self.source_files = [] - super(CoverallReporter, self).__init__(*args, **kwargs) + self.reporter = Reporter(*args, **kwargs) def report(self, morfs=None): """ @@ -30,28 +30,30 @@ def report(self, morfs=None): `outfile` is a file object to write the json to. """ units = None - if hasattr(self, 'find_code_units'): - self.find_code_units(morfs) + if hasattr(self.reporter, 'find_code_units'): + self.reporter.find_code_units(morfs) else: - units = self.find_file_reporters(morfs) + units = self.reporter.find_file_reporters(morfs) if units is None: - if hasattr(self, 'code_units'): - units = self.code_units + if hasattr(self.reporter, 'code_units'): + units = self.reporter.code_units else: - units = self.file_reporters + units = self.reporter.file_reporters for cu in units: try: - analyzed = self.coverage._analyze(cu) # pylint: disable=W0212 + _fn = self.reporter.coverage._analyze # pylint: disable=W0212 + analyzed = _fn(cu) self.parse_file(cu, analyzed) except NoSource: - if not self.config.ignore_errors: + if not self.reporter.config.ignore_errors: log.warning('No source for %s', cu.filename) except NotPython: # Only report errors for .py files, and only if we didn't # explicitly suppress those errors. - if cu.should_be_python() and not self.config.ignore_errors: + if (cu.should_be_python() + and not self.reporter.config.ignore_errors): log.warning('Source file is not python %s', cu.filename) except KeyError: version = [int(x) for x in __version__.split('.')] From 4b1068c3266af8329f27a8140a92a1fdd67b5550 Mon Sep 17 00:00:00 2001 From: Kevin James Date: Mon, 30 Dec 2019 17:57:54 -0600 Subject: [PATCH 3/5] refactor: consolidate v4/v5 coverage logic split --- coveralls/api.py | 2 +- coveralls/reporter.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/coveralls/api.py b/coveralls/api.py index 2215ed68..204fab6d 100644 --- a/coveralls/api.py +++ b/coveralls/api.py @@ -265,7 +265,7 @@ def get_coverage(self): else: workman.get_data() - return CoverallReporter(workman, workman.config).report() + return CoverallReporter(workman, workman.config).coverage @staticmethod def debug_bad_encoding(data): diff --git a/coveralls/reporter.py b/coveralls/reporter.py index 57aa4cfd..c7eb6733 100644 --- a/coveralls/reporter.py +++ b/coveralls/reporter.py @@ -7,7 +7,6 @@ from coverage.misc import NoSource from coverage.misc import NotPython from coverage.phystokens import source_encoding -from coverage.report import Reporter from .exception import CoverallsException @@ -18,17 +17,23 @@ class CoverallReporter(object): """Custom coverage.py reporter for coveralls.io""" - def __init__(self, *args, **kwargs): - self.source_files = [] - self.reporter = Reporter(*args, **kwargs) + def __init__(self, cov, conf): + self.coverage = [] + self.report(cov, conf) - def report(self, morfs=None): + def report(self, cov, conf, morfs=None): """ Generate a part of json report for coveralls `morfs` is a list of modules or filenames. `outfile` is a file object to write the json to. """ + try: + from coverage.report import Reporter + self.reporter = Reporter(cov, conf) + except ImportError: # coverage >= 5.0 + raise Exception('TODO: coverage v5 compatibility') + units = None if hasattr(self.reporter, 'find_code_units'): self.reporter.find_code_units(morfs) @@ -68,7 +73,7 @@ def report(self, morfs=None): raise - return self.source_files + return self.coverage @staticmethod def get_hits(line_num, analysis): @@ -162,4 +167,4 @@ def parse_file(self, cu, analysis): if branches: results['branches'] = branches - self.source_files.append(results) + self.coverage.append(results) From bcac2377c974f6d2aaf935d9d96bba173ccc9d78 Mon Sep 17 00:00:00 2001 From: Kevin James Date: Mon, 30 Dec 2019 18:42:31 -0600 Subject: [PATCH 4/5] feat: support for coverage5 --- coveralls/reporter.py | 92 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/coveralls/reporter.py b/coveralls/reporter.py index c7eb6733..3d237d17 100644 --- a/coveralls/reporter.py +++ b/coveralls/reporter.py @@ -21,6 +21,86 @@ def __init__(self, cov, conf): self.coverage = [] self.report(cov, conf) + def report5(self, cov): + # N.B. this method is 99% copied from the coverage source code; + # unfortunately, the coverage v5 style of `get_analysis_to_report` + # errors out entirely if any source file has issues -- which would be a + # breaking change for us. In the interest of backwards compatibility, + # I've copied their code here so we can maintain the same `coveralls` + # API regardless of which `coverage` version is being used. + # + # TODO: deprecate the relevant APIs so we can just use the coverage + # public API directly. + # + # from coverage.report import get_analysis_to_report + # try: + # for cu, analyzed in get_analysis_to_report(cov, None): + # self.parse_file(cu, analyzed) + # except NoSource: + # # Note that this behavior must necessarily change between + # # coverage<5 and coverage>=5, as we are no longer interweaving + # # with get_analysis_to_report (a single exception breaks the + # # whole loop) + # log.warning('No source for at least one file') + # except NotPython: + # # Note that this behavior must necessarily change between + # # coverage<5 and coverage>=5, as we are no longer interweaving + # # with get_analysis_to_report (a single exception breaks the + # # whole loop) + # log.warning('A source file is not python') + # except CoverageException as e: + # if str(e) != 'No data to report.': + # raise + + from coverage.files import FnmatchMatcher, prep_patterns + + # get_analysis_to_report starts here; changes marked with TODOs + file_reporters = cov._get_file_reporters(None) # pylint: disable=W0212 + config = cov.config + + if config.report_include: + matcher = FnmatchMatcher(prep_patterns(config.report_include)) + file_reporters = [fr for fr in file_reporters + if matcher.match(fr.filename)] + + if config.report_omit: + matcher = FnmatchMatcher(prep_patterns(config.report_omit)) + file_reporters = [fr for fr in file_reporters + if not matcher.match(fr.filename)] + + # TODO: deprecate changes + # if not file_reporters: + # raise CoverageException("No data to report.") + + for fr in sorted(file_reporters): + try: + analysis = cov._analyze(fr) # pylint: disable=W0212 + except NoSource: + if not config.ignore_errors: + # TODO: deprecate changes + # raise + log.warning('No source for %s', fr.filename) + except NotPython: + # Only report errors for .py files, and only if we didn't + # explicitly suppress those errors. + # NotPython is only raised by PythonFileReporter, which has a + # should_be_python() method. + if fr.should_be_python(): + if config.ignore_errors: + msg = "Couldn't parse Python file '{}'".format( + fr.filename) + cov._warn(msg, # pylint: disable=W0212 + slug="couldnt-parse") + else: + # TODO: deprecate changes + # raise + log.warning('Source file is not python %s', + fr.filename) + else: + # TODO: deprecate changes (well, this one is fine /shrug) + # yield (fr, analysis) + self.parse_file(fr, analysis) + def report(self, cov, conf, morfs=None): """ Generate a part of json report for coveralls @@ -28,11 +108,12 @@ def report(self, cov, conf, morfs=None): `morfs` is a list of modules or filenames. `outfile` is a file object to write the json to. """ + # pylint: disable=too-many-branches try: from coverage.report import Reporter self.reporter = Reporter(cov, conf) except ImportError: # coverage >= 5.0 - raise Exception('TODO: coverage v5 compatibility') + return self.report5(cov) units = None if hasattr(self.reporter, 'find_code_units'): @@ -108,7 +189,14 @@ def get_arcs(analysis): if not analysis.has_arcs(): return None - branch_lines = analysis.branch_lines() + if not hasattr(analysis, 'branch_lines'): + # N.B. switching to the public method analysis.missing_branch_arcs + # would work for half of what we need, but there doesn't seem to be + # an equivalent analysis.executed_branch_arcs + branch_lines = analysis._branch_lines() # pylint: disable=W0212 + else: + branch_lines = analysis.branch_lines() + branches = [] for l1, l2 in analysis.arcs_executed(): From 062de685a3c61eef43d3d14cb7b9c3a54b41278c Mon Sep 17 00:00:00 2001 From: Kevin James Date: Mon, 30 Dec 2019 18:50:41 -0600 Subject: [PATCH 5/5] chore: split coverage versions in ci --- .circleci/config.yml | 82 ++++++++++++++++++++++++++++++++++++++------ tox.ini | 16 ++++++--- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8df8c5a4..51cd9935 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,13 +13,26 @@ jobs: # TODO: figure out `<>.replace('.','')` tox_environment: type: string + cov_version: + default: "" + type: string steps: - run: apk add --no-cache gcc git libffi-dev musl-dev openssh-client openssl-dev - checkout - run: pip install tox tox-factor - - run: tox -f <> - - run: tox -e coveralls + - unless: + condition: <> + steps: + - run: tox -f <> + - run: tox -e coveralls41 + - when: + condition: <> + steps: + - run: tox -f <>-cov<> + - run: tox -e coveralls<> + # TODO: by introspecting `docker_image` (or `tox_environment`), we could + # these job definitions toxpypy: docker: - image: pypy:<> @@ -29,11 +42,22 @@ jobs: # TODO: figure out `<>.replace('.','')` tox_environment: type: string + cov_version: + default: "" + type: string steps: - checkout - run: pip install tox tox-factor - - run: tox -f <> - - run: tox -e coveralls + - unless: + condition: <> + steps: + - run: tox -f <> + - run: tox -e coveralls41 + - when: + condition: <> + steps: + - run: tox -f <>-cov<> + - run: tox -e coveralls<> workflows: run-jobs: @@ -54,17 +78,35 @@ workflows: docker_image: '3.5' tox_environment: py35 - toxpy: - name: test-py3.6 + name: test-py3.6-cov4 + docker_image: '3.6' + tox_environment: py36 + cov_version: '41' + - toxpy: + name: test-py3.6-cov5 docker_image: '3.6' tox_environment: py36 + cov_version: '5' - toxpy: - name: test-py3.7 + name: test-py3.7-cov4 docker_image: '3.7' tox_environment: py37 + cov_version: '41' + - toxpy: + name: test-py3.7-cov5 + docker_image: '3.7' + tox_environment: py37 + cov_version: '5' + - toxpy: + name: test-py3.8-cov4 + docker_image: '3.8' + tox_environment: py38 + cov_version: '41' - toxpy: - name: test-py3.8 + name: test-py3.8-cov5 docker_image: '3.8' tox_environment: py38 + cov_version: '5' - toxpypy: name: test-pypy2-5 @@ -80,14 +122,32 @@ workflows: tox_environment: pypy - toxpypy: - name: test-pypy3-5 + name: test-pypy3-5-cov4 docker_image: 3-5 tox_environment: pypy3 + cov_version: '41' - toxpypy: - name: test-pypy3-6 + name: test-pypy3-5-cov5 + docker_image: 3-5 + tox_environment: pypy3 + cov_version: '5' + - toxpypy: + name: test-pypy3-6-cov4 + docker_image: 3-6 + tox_environment: pypy3 + cov_version: '41' + - toxpypy: + name: test-pypy3-6-cov5 docker_image: 3-6 tox_environment: pypy3 + cov_version: '5' - toxpypy: - name: test-pypy3-7 + name: test-pypy3-7-cov4 docker_image: 3-7 - tox_environment: pypy37 + tox_environment: pypy3 + cov_version: '41' + - toxpypy: + name: test-pypy3-7-cov5 + docker_image: 3-7 + tox_environment: pypy3 + cov_version: '5' diff --git a/tox.ini b/tox.ini index dd5ed0d1..2ba185b7 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,9 @@ envlist = py{27,34,35,py}-cov{3,4,41}-{default,pyyaml},py{36,37,38,py3}-cov{41,5 python = 2.7: py27 3.5: py35 - 3.6: py36 - 3.7: py37 - 3.8: py38 + 3.6: py36-cov5 + 3.7: py37-cov5 + 3.8: py38-cov5 [testenv] passenv = * @@ -25,8 +25,14 @@ commands = coverage run --branch --source=coveralls -m pytest tests/ coverage report -m -[testenv:coveralls] +[testenv:coveralls41] deps = - coverage<6.0 + coverage>=4.1,<5.0 +commands = + coveralls --verbose + +[testenv:coveralls5] +deps = + coverage>=5.0,<6.0 commands = coveralls --verbose