Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support coverage>=5.0 #214

Merged
merged 5 commits into from Dec 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 71 additions & 11 deletions .circleci/config.yml
Expand Up @@ -13,13 +13,26 @@ jobs:
# TODO: figure out `<<parameters.docker_image>>.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 <<parameters.tox_environment>>
- run: tox -e coveralls
- unless:
condition: <<parameters.cov_version>>
steps:
- run: tox -f <<parameters.tox_environment>>
- run: tox -e coveralls41
- when:
condition: <<parameters.cov_version>>
steps:
- run: tox -f <<parameters.tox_environment>>-cov<<parameters.cov_version>>
- run: tox -e coveralls<<parameters.cov_version>>

# TODO: by introspecting `docker_image` (or `tox_environment`), we could
# these job definitions
toxpypy:
docker:
- image: pypy:<<parameters.docker_image>>
Expand All @@ -29,11 +42,22 @@ jobs:
# TODO: figure out `<<parameters.docker_image>>.replace('.','')`
tox_environment:
type: string
cov_version:
default: ""
type: string
steps:
- checkout
- run: pip install tox tox-factor
- run: tox -f <<parameters.tox_environment>>
- run: tox -e coveralls
- unless:
condition: <<parameters.cov_version>>
steps:
- run: tox -f <<parameters.tox_environment>>
- run: tox -e coveralls41
- when:
condition: <<parameters.cov_version>>
steps:
- run: tox -f <<parameters.tox_environment>>-cov<<parameters.cov_version>>
- run: tox -e coveralls<<parameters.cov_version>>

workflows:
run-jobs:
Expand All @@ -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
Expand All @@ -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'
2 changes: 1 addition & 1 deletion coveralls/api.py
Expand Up @@ -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):
Expand Down
131 changes: 113 additions & 18 deletions coveralls/reporter.py
Expand Up @@ -7,51 +7,139 @@
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


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)
def __init__(self, cov, conf):
self.coverage = []
self.report(cov, conf)

def report(self, morfs=None):
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

`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
return self.report5(cov)

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('.')]
Expand All @@ -66,7 +154,7 @@ def report(self, morfs=None):

raise

return self.source_files
return self.coverage

@staticmethod
def get_hits(line_num, analysis):
Expand Down Expand Up @@ -101,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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nedbat is there anything I missed in the public coverage v5+ API which would support this? Specifically, its the following block that I couldn't work out how to do without private APIs:

for l1, l2 in analysis.arcs_executed():
    if l1 in branch_lines:
        # SNIP

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheKevJames let's talk about this in another place. Clearly the Coverage public API doesn't give you everything you need yet. You called out this spot, but looking at the rest of the file, there are many things you are using from coverage.py that I didn't anticipate you would need.

I've added an issue for coverage.py, let's discuss the details there: nedbat/coveragepy#921

# 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():
Expand Down Expand Up @@ -160,4 +255,4 @@ def parse_file(self, cu, analysis):
if branches:
results['branches'] = branches

self.source_files.append(results)
self.coverage.append(results)
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -33,7 +33,7 @@
],
},
install_requires=[
'coverage>=3.6,<5.0',
'coverage>=3.6,<6.0',
'docopt>=0.6.1',
'requests>=1.0.0',
],
Expand Down
18 changes: 12 additions & 6 deletions tox.ini
Expand Up @@ -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 = *
Expand All @@ -20,13 +20,19 @@ 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]
[testenv:coveralls41]
deps =
coverage<5.0
coverage>=4.1,<5.0
commands =
coveralls --verbose

[testenv:coveralls5]
deps =
coverage>=5.0,<6.0
commands =
coveralls --verbose