├── MANIFEST.in ├── tests ├── __init__.py ├── test_cli.py └── test_api.py ├── example ├── __init__.py ├── runtests.py └── project.py ├── nonunicode ├── __init__.py └── nonunicode.py ├── coveralls ├── __init__.py ├── cli.py ├── reporter.py └── api.py ├── tox.ini ├── AUTHORS ├── .travis.yml ├── CONTRIBUTING.md ├── .gitignore ├── CHANGELOG.rst ├── LICENCE ├── setup.py └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 -------------------------------------------------------------------------------- /nonunicode/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 -------------------------------------------------------------------------------- /coveralls/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from .api import Coveralls 4 | 5 | __all__ = ['api'] -------------------------------------------------------------------------------- /nonunicode/nonunicode.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex/coveralls-python/master/nonunicode/nonunicode.py -------------------------------------------------------------------------------- /example/runtests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from project import hello 3 | 4 | if __name__ == '__main__': 5 | hello() -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py32, py33 3 | 4 | [testenv] 5 | deps = pytest 6 | commands = {envpython} setup.py test 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Coveralls is written and maintained by Ilya Baryshev and various contributors: 2 | 3 | * Damian Fuentes 4 | * Yaroslav Halchenko 5 | * Steve Lamb 6 | * Aaron Meurer 7 | -------------------------------------------------------------------------------- /example/project.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | def hello(): 5 | print('world') 6 | 7 | 8 | class Foo(object): 9 | """ Bar """ 10 | 11 | 12 | def baz(): 13 | print('this is not tested') -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - 3.2 6 | - 3.3 7 | - pypy 8 | install: 9 | python setup.py develop 10 | script: 11 | - coverage run --source=coveralls setup.py test 12 | - coverage report -m 13 | after_script: 14 | coveralls --verbose 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Issues 2 | ====== 3 | If you're opening a support ticket about coveralls not working, 4 | please add ``coveralls debug`` output. 5 | 6 | Pull requests 7 | ============= 8 | Pull requests with untested code will not be merged straight away. 9 | If you woud like to speed up the process, please, write tests. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | __pycache__ 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | .coveralls.yml 38 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 0.4 (TBA) 5 | ~~~~~~~~~~~ 6 | * Added support for --rcfile= option to cli 7 | * Improved docs: nosetests and troubleshooting sections added 8 | * Added debug in case of UnicodeDecodeError 9 | 10 | 0.3 (2013-10-02) 11 | ~~~~~~~~~~~~~~~~ 12 | * Added initial support for Circle CI 13 | * Fixed Unicode not defined error in python 3 14 | 15 | 0.2 (2013-05-26) 16 | ~~~~~~~~~~~~~~~~ 17 | * Python 3.2 and PyPy support 18 | * Graceful handling of coverage exceptions 19 | * Fixed UnicodeDecodeError in json encoding 20 | * Improved readme 21 | 22 | 0.1.1 (2013-02-13) 23 | ~~~~~~~~~~~~~~~~~~ 24 | * Introduced COVERALLS_REPO_TOKEN environment variable as a fallback for Travis 25 | * Removed repo_token from verbose output for security reasons 26 | 27 | 0.1 (2013-02-12) 28 | ~~~~~~~~~~~~~~~~ 29 | * Initial release 30 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 by Ilya Baryshev and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | 4 | from mock import patch, call 5 | import pytest 6 | 7 | import coveralls 8 | from coveralls.api import CoverallsException 9 | import coveralls.cli 10 | 11 | 12 | @patch.dict(os.environ, {'TRAVIS': 'True'}, clear=True) 13 | @patch.object(coveralls.cli.log, 'info') 14 | @patch.object(coveralls.Coveralls, 'wear') 15 | def test_debug(mock_wear, mock_log): 16 | coveralls.cli.main(argv=['debug']) 17 | mock_wear.assert_called_with(dry_run=True) 18 | mock_log.assert_has_calls([call("Testing coveralls-python...")]) 19 | 20 | 21 | @patch.object(coveralls.cli.log, 'info') 22 | @patch.object(coveralls.Coveralls, 'wear') 23 | @patch.dict(os.environ, {'TRAVIS': 'True'}, clear=True) 24 | def test_real(mock_wear, mock_log): 25 | coveralls.cli.main(argv=[]) 26 | mock_wear.assert_called_with() 27 | mock_log.assert_has_calls([call("Submitting coverage to coveralls.io..."), call("Coverage submitted!")]) 28 | 29 | 30 | @patch.dict(os.environ, {'TRAVIS': 'True'}, clear=True) 31 | @patch('coveralls.cli.Coveralls') 32 | def test_rcfile(mock_coveralls): 33 | coveralls.cli.main(argv=['--rcfile=coveragerc']) 34 | mock_coveralls.assert_called_with(config_file='coveragerc') 35 | 36 | exc = CoverallsException('bad stuff happened') 37 | 38 | @patch.object(coveralls.cli.log, 'error') 39 | @patch.object(coveralls.Coveralls, 'wear', side_effect=exc) 40 | @patch.dict(os.environ, {'TRAVIS': 'True'}, clear=True) 41 | def test_exception(mock_coveralls, mock_log): 42 | coveralls.cli.main(argv=[]) 43 | mock_log.assert_has_calls([call(exc)]) 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools.command.test import test as TestCommand 3 | from setuptools import setup 4 | 5 | 6 | class PyTest(TestCommand): 7 | def finalize_options(self): 8 | TestCommand.finalize_options(self) 9 | self.test_args = [] 10 | self.test_suite = True 11 | 12 | def run_tests(self): 13 | import pytest 14 | errno = pytest.main(self.test_args) 15 | sys.exit(errno) 16 | 17 | 18 | setup( 19 | name='coveralls', 20 | version='0.4dev', 21 | packages=['coveralls'], 22 | url='http://github.com/coagulant/coveralls-python', 23 | license='MIT', 24 | author='Ilya Baryshev', 25 | author_email='baryshev@gmail.com', 26 | description='Show coverage stats online via coveralls.io', 27 | long_description=open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read(), 28 | entry_points={ 29 | 'console_scripts': [ 30 | 'coveralls = coveralls.cli:main', 31 | ], 32 | }, 33 | install_requires=['PyYAML>=3.10', 'docopt>=0.6.1', 'coverage>=3.6', 'requests>=1.0.0', 'sh>=1.08'], 34 | tests_require=['mock', 'pytest'], 35 | cmdclass={'test': PyTest}, 36 | classifiers=[ 37 | 'Development Status :: 4 - Beta', 38 | 'Topic :: Software Development :: Testing', 39 | 'Intended Audience :: Developers', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python', 43 | 'Programming Language :: Python :: 2.6', 44 | 'Programming Language :: Python :: 2.7', 45 | 'Programming Language :: Python :: 3.2', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Programming Language :: Python :: Implementation :: PyPy', 48 | 'Programming Language :: Python :: Implementation :: CPython', 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /coveralls/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Publish coverage results online via coveralls.io 3 | 4 | Puts your coverage results on coveralls.io for everyone to see. 5 | It makes custom report for data generated by coverage.py package and sends it to `json API`_ of coveralls.io service. 6 | All python files in your coverage analysis are posted to this service along with coverage stats, 7 | so please make sure you're not ruining your own security! 8 | 9 | Usage: 10 | coveralls [options] 11 | coveralls debug [options] 12 | 13 | Debug mode doesn't send anything, just outputs json to stdout, useful for development. 14 | It also forces verbose output. 15 | 16 | Global options: 17 | --rcfile= Specify configuration file. [default: .coveragerc] 18 | -h --help Display this help 19 | -v --verbose Print extra info, True for debug command 20 | 21 | Example: 22 | $ coveralls 23 | Submitting coverage to coveralls.io... 24 | Coverage submitted! 25 | Job #38.1 26 | https://coveralls.io/jobs/92059 27 | """ 28 | import logging 29 | from docopt import docopt 30 | from coveralls import Coveralls 31 | from coveralls.api import CoverallsException 32 | 33 | 34 | log = logging.getLogger('coveralls') 35 | 36 | 37 | def main(argv=None): 38 | options = docopt(__doc__, argv=argv) 39 | if options['debug']: 40 | options['--verbose'] = True 41 | level = logging.DEBUG if options['--verbose'] else logging.INFO 42 | log.addHandler(logging.StreamHandler()) 43 | log.setLevel(level) 44 | 45 | try: 46 | coverallz = Coveralls(config_file=options['--rcfile']) 47 | if not options['debug']: 48 | log.info("Submitting coverage to coveralls.io...") 49 | result = coverallz.wear() 50 | log.info("Coverage submitted!") 51 | log.info(result['message']) 52 | log.info(result['url']) 53 | log.debug(result) 54 | else: 55 | log.info("Testing coveralls-python...") 56 | coverallz.wear(dry_run=True) 57 | except KeyboardInterrupt: # pragma: no cover 58 | log.info('Aborted') 59 | except CoverallsException as e: 60 | log.error(e) 61 | except Exception: # pragma: no cover 62 | raise 63 | -------------------------------------------------------------------------------- /coveralls/reporter.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import logging 3 | import sys 4 | 5 | from coverage.misc import NoSource, NotPython 6 | from coverage.phystokens import source_encoding 7 | from coverage.report import Reporter 8 | 9 | 10 | log = logging.getLogger('coveralls') 11 | 12 | 13 | class CoverallReporter(Reporter): 14 | """ Custom coverage.py reporter for coveralls.io 15 | """ 16 | def report(self, morfs=None): 17 | """ Generate a part of json report for coveralls 18 | 19 | `morfs` is a list of modules or filenames. 20 | `outfile` is a file object to write the json to. 21 | """ 22 | self.source_files = [] 23 | self.find_code_units(morfs) 24 | 25 | for cu in self.code_units: 26 | try: 27 | self.parse_file(cu, self.coverage._analyze(cu)) 28 | except NoSource: 29 | if not self.config.ignore_errors: 30 | log.warn('No source for %s', cu.name) 31 | except NotPython: 32 | # Only report errors for .py files, and only if we didn't 33 | # explicitly suppress those errors. 34 | if cu.should_be_python() and not self.config.ignore_errors: 35 | log.warn('Source file is not python %s', cu.name) 36 | 37 | return self.source_files 38 | 39 | def get_hits(self, line_num, analysis): 40 | """ Source file stats for each line. 41 | 42 | * A positive integer if the line is covered, 43 | representing the number of times the line is hit during the test suite. 44 | * 0 if the line is not covered by the test suite. 45 | * null to indicate the line is not relevant to code coverage 46 | (it may be whitespace or a comment). 47 | """ 48 | if line_num in analysis.missing: 49 | return 0 50 | if line_num in analysis.statements: 51 | return 1 52 | return None 53 | 54 | def parse_file(self, cu, analysis): 55 | """ Generate data for single file """ 56 | filename = cu.file_locator.relative_filename(cu.filename) 57 | coverage_lines = [self.get_hits(i, analysis) for i in range(1, len(analysis.parser.lines) + 1)] 58 | source_file = cu.source_file() 59 | try: 60 | source = source_file.read() 61 | if sys.version_info < (3, 0): 62 | encoding = source_encoding(source) 63 | if encoding != 'utf-8': 64 | source = source.decode(encoding).encode('utf-8') 65 | finally: 66 | source_file.close() 67 | self.source_files.append({ 68 | 'name': filename, 69 | 'source': source, 70 | 'coverage': coverage_lines 71 | }) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Coveralls for python 2 | ==================== 3 | 4 | .. image:: https://travis-ci.org/coagulant/coveralls-python.png?branch=master 5 | :target: https://travis-ci.org/coagulant/coveralls-python 6 | 7 | .. image:: https://coveralls.io/repos/coagulant/coveralls-python/badge.png?branch=master 8 | :target: https://coveralls.io/r/coagulant/coveralls-python 9 | 10 | .. image:: https://pypip.in/v/coveralls/badge.png 11 | :target: https://crate.io/packages/coveralls/ 12 | 13 | `Coveralls.io`_ is service to publish your coverage stats online with a lot of `nice features`_. 14 | This package provides seamless integration with ``coverage.py`` in your python projects. 15 | For ruby projects, there is an `official gem`_. 16 | Only projects hosted on Github are supported. 17 | 18 | Works with python 2.6+, 3.2+ and pypy 1.9. 19 | 20 | .. _Coveralls.io: http://coveralls.io 21 | .. _nice features: https://coveralls.io/info/features 22 | .. _official gem: https://coveralls.io/docs/ruby 23 | 24 | Usage (Travis CI) 25 | ----------------- 26 | 27 | This library will publish your coverage results on coveralls.io for everyone to see (unless you're using pro account). 28 | This package can possibly work with different CI environments, but it's only tested to work with `Travis CI`_ atm. 29 | 30 | 1. First, log in via Github and `add your repo`_ on Coveralls website. 31 | 2. Add ``pip install coveralls`` to ``install`` section of ``.travis.yml`` 32 | 3. Make sure you run your tests with coverage during the build in ``script`` part. Example:: 33 | 34 | # --source specifies what packages to cover, you probably want to use that option 35 | script: 36 | coverage run --source=yourpackagename setup.py test 37 | 38 | Note, that example command will gather coverage for specified package. 39 | If you wish to customize what's included in your reports, consult `coverage docs`_. 40 | 41 | .. _coverage docs: http://nedbatchelder.com/code/coverage/ 42 | 43 | 4. Execute run ``coveralls`` in ``after_success`` section:: 44 | 45 | after_success: 46 | coveralls 47 | 48 | Full example of .travis.yml:: 49 | 50 | language: python 51 | python: 52 | - 2.7 53 | - 3.3 54 | install: 55 | - pip install -r requirements.txt 56 | - pip install coveralls 57 | script: 58 | coverage run --source=moscowdjango,meetup manage.py test 59 | after_success: 60 | coveralls 61 | 62 | Usage (another CI) 63 | ~~~~~~~~~~~~~~~~~~ 64 | 65 | If you're NOT using Travis, first option is to provide a ``repo_token`` option in ``.coveralls.yml`` 66 | at the root of your repo. This is your own secret token, which is available at the bottom of your repository's page on Coveralls. 67 | Make sure it stays **secret**, do not put it in your public repo. 68 | 69 | Example of .coveralls.yml:: 70 | 71 | # .coveralls.yml 72 | repo_token: TjkDuVpGjuQcRhNW8dots9c8SSnv7ReM5 73 | 74 | Another alternative is to use ``COVERALLS_REPO_TOKEN`` env variable. 75 | 76 | .. _add your repo: https://coveralls.io/repos/new 77 | .. _Travis CI: http://travis-ci.org 78 | 79 | 80 | Nosetests 81 | ~~~~~~~~~ 82 | 83 | `Nosetests`_ provide a plugin for coverage measurement of your code:: 84 | 85 | $ nosetests --with-coverage --cover-package= 86 | 87 | However, it gathers coverage for all executed code, ignoring ``source`` config option in ``.coveragerc``. 88 | It means, that ``coveralls`` will report unnecessary files, which is inconvenient. 89 | Here is a workaround, use ``omit`` option in your ``.coverage.rc``to specify a list of filename patterns, 90 | the files to leave out of reporting (your paths might differ) :: 91 | 92 | [report] 93 | omit = 94 | */python?.?/* 95 | */site-packages/nose/* 96 | 97 | Note, that native coverage.py and py.test are not affected by this problem and do not require this workaround. 98 | 99 | .. _Nosetests:http://nose.readthedocs.org/en/latest/plugins/cover.html 100 | 101 | How it works 102 | ------------ 103 | It makes custom report for data generated by ``coverage.py`` package and sends it to `json API`_ of coveralls.io service. 104 | All python files in your coverage analysis are posted to this service along with coverage stats, 105 | so please make sure you're not ruining your own security! For private projects there is `Coveralls Pro`_. 106 | 107 | .. _json API: https://coveralls.io/docs/api_reference 108 | .. _Coveralls Pro: https://coveralls.io/docs/pro 109 | 110 | 111 | Tips for .coveragerc config 112 | --------------------------- 113 | 114 | This section is a list of most common options for coverage.py, which collects all the data. 115 | Coveralls feeds from this data, so it's good to know `how to to configure coverage.py`_. 116 | 117 | To limit the `report with only your packages`_, specify their names (or directories):: 118 | 119 | [run] 120 | source = pkgname,your_otherpackage 121 | 122 | To exclude parts of your source from coverage, for example migrations folders:: 123 | 124 | [report] 125 | omit = */migrations/* 126 | 127 | Some lines are never executed in your tests, but that can be ok. 128 | To mark those lines use inline comments right in your source code:: 129 | 130 | if debug: # pragma: no cover 131 | msg = "blah blah" 132 | log_message(msg, a) 133 | 134 | Sometimes it can be tedious to mark them in code, so you can `specify whole lines to .coveragerc`_:: 135 | 136 | [report] 137 | exclude_lines = 138 | pragma: no cover 139 | def __repr__ 140 | raise AssertionError 141 | raise NotImplementedError 142 | if __name__ == .__main__.: 143 | 144 | Finally, if you're using non-default configuration file, specify it to coveralls command:: 145 | 146 | $ coveralls --rcfile= 147 | 148 | .. _how to to configure coverage.py: http://nedbatchelder.com/code/coverage/config.html 149 | .. _report with only your packages: http://nedbatchelder.com/code/coverage/source.html#source 150 | .. _specify whole lines to .coveragerc: http://nedbatchelder.com/code/coverage/excluding.html 151 | 152 | Troubleshooting 153 | --------------- 154 | 155 | In case your coverage is not submitted to coveralls.io, despite your best efforts to configure, 156 | you can use debug:: 157 | 158 | $ coveralls debug 159 | 160 | Debug mode doesn't send anything, just outputs prepared json and reported files list to stdout. 161 | 162 | Contributing 163 | ----------- 164 | 165 | Run tests:: 166 | 167 | $ python setup.py test 168 | 169 | Install latest `unstable version`_:: 170 | 171 | $ pip install coveralls==dev 172 | 173 | .. _unstable version: https://github.com/coagulant/coveralls-python/archive/master.zip#egg=coveralls-dev 174 | 175 | 176 | .. image:: https://d2weczhvl823v0.cloudfront.net/coagulant/coveralls-python/trend.png 177 | :alt: Bitdeli badge 178 | :target: https://bitdeli.com/free 179 | 180 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | import json 4 | import os 5 | from os.path import join, dirname 6 | import re 7 | import shutil 8 | import tempfile 9 | import unittest 10 | import coverage 11 | 12 | import sh 13 | from mock import patch 14 | import pytest 15 | 16 | from coveralls import Coveralls 17 | from coveralls.api import log 18 | 19 | 20 | class GitBasedTest(unittest.TestCase): 21 | 22 | def setUp(self): 23 | self.dir = tempfile.mkdtemp() 24 | sh.cd(self.dir) 25 | sh.git.init() 26 | sh.git('config', 'user.name', '"Guido"') 27 | sh.git('config', 'user.email', '"me@here.com"') 28 | sh.touch('README') 29 | sh.git.add('README') 30 | sh.git.commit('-m', 'first commit') 31 | sh.git('remote', 'add', 'origin', 'https://github.com/username/Hello-World.git') 32 | 33 | def tearDown(self): 34 | shutil.rmtree(self.dir) 35 | 36 | 37 | @patch.object(Coveralls, 'config_filename', '.coveralls.mock') 38 | class Configration(unittest.TestCase): 39 | 40 | def setUp(self): 41 | with open('.coveralls.mock', 'w+') as fp: 42 | fp.write('repo_token: xxx\n') 43 | fp.write('service_name: jenkins\n') 44 | 45 | def tearDown(self): 46 | os.remove('.coveralls.mock') 47 | 48 | @patch.dict(os.environ, {}, clear=True) 49 | def test_local_with_config(self): 50 | cover = Coveralls() 51 | assert cover.config['service_name'] == 'jenkins' 52 | assert cover.config['repo_token'] == 'xxx' 53 | assert 'service_job_id' not in cover.config 54 | 55 | 56 | @patch.object(Coveralls, 'config_filename', '.coveralls.mock') 57 | class NoConfig(unittest.TestCase): 58 | 59 | @patch.dict(os.environ, {'TRAVIS': 'True', 'TRAVIS_JOB_ID': '777'}, clear=True) 60 | def test_travis_no_config(self): 61 | cover = Coveralls() 62 | assert cover.config['service_name'] == 'travis-ci' 63 | assert cover.config['service_job_id'] == '777' 64 | assert 'repo_token' not in cover.config 65 | 66 | @patch.dict(os.environ, {'TRAVIS': 'True', 'TRAVIS_JOB_ID': '777', 'COVERALLS_REPO_TOKEN': 'yyy'}, clear=True) 67 | def test_repo_token_from_env(self): 68 | cover = Coveralls() 69 | assert cover.config['service_name'] == 'travis-ci' 70 | assert cover.config['service_job_id'] == '777' 71 | assert cover.config['repo_token'] == 'yyy' 72 | 73 | @patch.dict(os.environ, {}, clear=True) 74 | def test_misconfigured(self): 75 | with pytest.raises(Exception) as excinfo: 76 | Coveralls() 77 | 78 | assert str(excinfo.value) == 'You have to provide either repo_token in .coveralls.mock, or launch via Travis' 79 | 80 | 81 | class Git(GitBasedTest): 82 | 83 | @patch.dict(os.environ, {'TRAVIS_BRANCH': 'master'}, clear=True) 84 | def test_git(self): 85 | cover = Coveralls(repo_token='xxx') 86 | git_info = cover.git_info() 87 | commit_id = git_info['git']['head'].pop('id') 88 | 89 | assert re.match(r'^[a-f0-9]{40}$', commit_id) 90 | assert git_info == {'git': { 91 | 'head': { 92 | 'committer_email': 'me@here.com', 93 | 'author_email': 'me@here.com', 94 | 'author_name': 'Guido', 95 | 'message': 'first commit', 96 | 'committer_name': 'Guido', 97 | }, 98 | 'remotes': [{ 99 | 'url': 'https://github.com/username/Hello-World.git', 100 | 'name': 'origin' 101 | }], 102 | 'branch': 'master' 103 | }} 104 | 105 | class ReporterTest(unittest.TestCase): 106 | 107 | def setUp(self): 108 | os.chdir(join(dirname(dirname(__file__)), 'example')) 109 | sh.rm('-f', '.coverage') 110 | sh.rm('-f', 'extra.py') 111 | self.cover = Coveralls(repo_token='xxx') 112 | 113 | def test_reporter(self): 114 | sh.coverage('run', 'runtests.py') 115 | assert self.cover.get_coverage() == [{ 116 | 'source': '# coding: utf-8\n\n\ndef hello():\n print(\'world\')\n\n\nclass Foo(object):\n """ Bar """\n\n\ndef baz():\n print(\'this is not tested\')', 117 | 'name': 'project.py', 118 | 'coverage': [None, None, None, 1, 1, None, None, 1, None, None, None, 1, 0]}, { 119 | 'source': "# coding: utf-8\nfrom project import hello\n\nif __name__ == '__main__':\n hello()", 120 | 'name': 'runtests.py', 'coverage': [None, 1, None, 1, 1]}] 121 | 122 | def test_missing_file(self): 123 | sh.echo('print("Python rocks!")', _out="extra.py") 124 | sh.coverage('run', 'extra.py') 125 | sh.rm('-f', 'extra.py') 126 | assert self.cover.get_coverage() == [] 127 | 128 | def test_not_python(self): 129 | sh.echo('print("Python rocks!")', _out="extra.py") 130 | sh.coverage('run', 'extra.py') 131 | sh.echo("

This isn't python!

", _out="extra.py") 132 | assert self.cover.get_coverage() == [] 133 | 134 | 135 | def test_non_unicode(): 136 | os.chdir(join(dirname(dirname(__file__)), 'nonunicode')) 137 | sh.coverage('run', 'nonunicode.py') 138 | expected_json_part = '"source": "# coding: iso-8859-15\\n\\ndef hello():\\n print (\'I like P\\u00f3lya distribution.\')"' 139 | assert expected_json_part in json.dumps(Coveralls(repo_token='xxx').get_coverage()) 140 | 141 | @patch('coveralls.api.requests') 142 | class WearTest(unittest.TestCase): 143 | 144 | def setUp(self): 145 | sh.rm('-f', '.coverage') 146 | 147 | def setup_mock(self, mock_requests): 148 | self.expected_json = {'url': 'https://coveralls.io/jobs/5869', 'message': 'Job #7.1 - 44.58% Covered'} 149 | mock_requests.post.return_value.json.return_value = self.expected_json 150 | 151 | def test_wet_run(self, mock_requests): 152 | self.setup_mock(mock_requests) 153 | result = Coveralls(repo_token='xxx').wear(dry_run=False) 154 | assert result == self.expected_json 155 | 156 | def test_dry_run(self, mock_requests): 157 | self.setup_mock(mock_requests) 158 | result = Coveralls(repo_token='xxx').wear(dry_run=True) 159 | assert result == {} 160 | 161 | @patch.object(log, 'debug') 162 | def test_repo_token_in_not_compromised_verbose(self, mock_logger, mock_requests): 163 | self.setup_mock(mock_requests) 164 | result = Coveralls(repo_token='xxx').wear(dry_run=True) 165 | assert 'xxx' not in mock_logger.call_args[0][0] 166 | 167 | def test_coveralls_unavailable(self, mock_requests): 168 | mock_requests.post.return_value.json.side_effect = ValueError 169 | mock_requests.post.return_value.status_code = 500 170 | mock_requests.post.return_value.text = 'Http 1./1 500' 171 | result = Coveralls(repo_token='xxx').wear() 172 | assert result == {'message': 'Failure to submit data. Response [500]: Http 1./1 500'} 173 | 174 | @patch('coveralls.reporter.CoverallReporter.report') 175 | def test_no_coverage(self, report_files, mock_requests): 176 | report_files.side_effect = coverage.CoverageException('No data to report') 177 | self.setup_mock(mock_requests) 178 | result = Coveralls(repo_token='xxx').wear() 179 | assert result == {'message': 'Failure to gather coverage: No data to report'} -------------------------------------------------------------------------------- /coveralls/api.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | import logging 4 | import os 5 | import re 6 | import coverage 7 | import requests 8 | import yaml 9 | from sh import git 10 | 11 | from .reporter import CoverallReporter 12 | 13 | 14 | log = logging.getLogger('coveralls') 15 | 16 | 17 | class CoverallsException(Exception): 18 | pass 19 | 20 | 21 | class Coveralls(object): 22 | config_filename = '.coveralls.yml' 23 | api_endpoint = 'https://coveralls.io/api/v1/jobs' 24 | default_client = 'coveralls-python' 25 | 26 | def __init__(self, **kwargs): 27 | """ Coveralls! 28 | 29 | * repo_token 30 | The secret token for your repository, found at the bottom of your repository's 31 | page on Coveralls. 32 | 33 | * service_name 34 | The CI service or other environment in which the test suite was run. 35 | This can be anything, but certain services have special features 36 | (travis-ci, travis-pro, or coveralls-ruby). 37 | 38 | * [service_job_id] 39 | A unique identifier of the job on the service specified by service_name. 40 | """ 41 | self._data = None 42 | self.config = kwargs 43 | file_config = self.load_config() or {} 44 | repo_token = self.config.get('repo_token') or file_config.get('repo_token', None) 45 | if repo_token: 46 | self.config['repo_token'] = repo_token 47 | 48 | if os.environ.get('TRAVIS'): 49 | is_travis = True 50 | self.config['service_name'] = file_config.get('service_name', None) or 'travis-ci' 51 | self.config['service_job_id'] = os.environ.get('TRAVIS_JOB_ID') 52 | else: 53 | is_travis = False 54 | self.config['service_name'] = file_config.get('service_name') or self.default_client 55 | 56 | if os.environ.get('COVERALLS_REPO_TOKEN', None): 57 | self.config['repo_token'] = os.environ.get('COVERALLS_REPO_TOKEN') 58 | 59 | if not self.config.get('repo_token') and not is_travis: 60 | raise CoverallsException('You have to provide either repo_token in %s, or launch via Travis' % self.config_filename) 61 | 62 | def load_config(self): 63 | try: 64 | return yaml.safe_load(open(os.path.join(os.getcwd(), self.config_filename))) 65 | except (OSError, IOError): 66 | log.debug('Missing %s file. Using only env variables.', self.config_filename) 67 | return {} 68 | 69 | def wear(self, dry_run=False): 70 | """ run! """ 71 | try: 72 | data = self.create_data() 73 | except coverage.CoverageException as e: 74 | return {'message': 'Failure to gather coverage: %s' % str(e)} 75 | try: 76 | json_string = json.dumps(data) 77 | except UnicodeDecodeError as e: 78 | log.error("ERROR: While preparing JSON received exception: %s" % e) 79 | self.debug_bad_encoding(data) 80 | raise 81 | if not dry_run: 82 | response = requests.post(self.api_endpoint, files={'json_file': json_string}) 83 | try: 84 | result = response.json() 85 | except ValueError: 86 | result = {'message': 'Failure to submit data. Response [%(status)s]: %(text)s' % { 87 | 'status': response.status_code, 88 | 'text': response.text}} 89 | else: 90 | result = {} 91 | json_string = re.sub(r'"repo_token": "(.+?)"', '"repo_token": "[secure]"', json_string) 92 | log.debug(json_string) 93 | log.debug("==\nReporting %s files\n==\n" % len(data['source_files'])) 94 | for source_file in data['source_files']: 95 | log.debug('%s - %s/%s' % (source_file['name'], 96 | sum(filter(None, source_file['coverage'])), 97 | len(source_file['coverage']))) 98 | return result 99 | 100 | def create_data(self): 101 | """ Generate object for api. 102 | Example json: 103 | { 104 | "service_job_id": "1234567890", 105 | "service_name": "travis-ci", 106 | "source_files": [ 107 | { 108 | "name": "example.py", 109 | "source": "def four\n 4\nend", 110 | "coverage": [null, 1, null] 111 | }, 112 | { 113 | "name": "two.py", 114 | "source": "def seven\n eight\n nine\nend", 115 | "coverage": [null, 1, 0, null] 116 | } 117 | ] 118 | } 119 | """ 120 | if not self._data: 121 | self._data = {'source_files': self.get_coverage()} 122 | self._data.update(self.git_info()) 123 | self._data.update(self.config) 124 | return self._data 125 | 126 | def get_coverage(self): 127 | workman = coverage.coverage(config_file=self.config.get('config_file', True)) 128 | workman.load() 129 | workman._harvest_data() 130 | reporter = CoverallReporter(workman, workman.config) 131 | return reporter.report() 132 | 133 | def git_info(self): 134 | """ A hash of Git data that can be used to display more information to users. 135 | 136 | Example: 137 | "git": { 138 | "head": { 139 | "id": "5e837ce92220be64821128a70f6093f836dd2c05", 140 | "author_name": "Wil Gieseler", 141 | "author_email": "wil@example.com", 142 | "committer_name": "Wil Gieseler", 143 | "committer_email": "wil@example.com", 144 | "message": "depend on simplecov >= 0.7" 145 | }, 146 | "branch": "master", 147 | "remotes": [{ 148 | "name": "origin", 149 | "url": "https://github.com/lemurheavy/coveralls-ruby.git" 150 | }] 151 | } 152 | """ 153 | 154 | git_info = {'git': { 155 | 'head': { 156 | 'id': gitlog('%H'), 157 | 'author_name': gitlog('%aN'), 158 | 'author_email': gitlog('%ae'), 159 | 'committer_name': gitlog('%cN'), 160 | 'committer_email': gitlog('%ce'), 161 | 'message': gitlog('%s'), 162 | }, 163 | 'branch': os.environ.get('CIRCLE_BRANCH') or os.environ.get('TRAVIS_BRANCH', git('rev-parse', '--abbrev-ref', 'HEAD').strip()), 164 | #origin git@github.com:coagulant/coveralls-python.git (fetch) 165 | 'remotes': [{'name': line.split()[0], 'url': line.split()[1]} 166 | for line in git.remote('-v') if '(fetch)' in line] 167 | }} 168 | return git_info 169 | 170 | def debug_bad_encoding(self, data): 171 | """ Let's try to help user figure out what is at fault""" 172 | at_fault_files = set() 173 | for source_file_data in data['source_files']: 174 | for key, value in source_file_data.items(): 175 | try: 176 | _ = json.dumps(value) 177 | except UnicodeDecodeError: 178 | at_fault_files.add(source_file_data['name']) 179 | if at_fault_files: 180 | log.error("HINT: Following files cannot be decoded properly into unicode." 181 | "Check their content: %s" % (', '.join(at_fault_files))) 182 | 183 | 184 | def gitlog(format): 185 | return str(git('--no-pager', 'log', "-1", pretty="format:%s" % format)) 186 | --------------------------------------------------------------------------------