├── tests
├── __init__.py
├── fixtures
│ ├── latin_1_source.py
│ ├── coverage.txt
│ ├── coverage_invalid_version.xml
│ ├── source.py
│ ├── coverage.xml
│ └── coverage_for_latin_1_source.xml
├── utils.py
├── test_argument_parser.py
├── test_api_client.py
├── test_reporter.py
├── test_ci.py
├── test_runner.py
└── test_formatter.py
├── codeclimate_test_reporter
├── VERSION
├── components
│ ├── __init__.py
│ ├── git_command.py
│ ├── api_client.py
│ ├── argument_parser.py
│ ├── payload_validator.py
│ ├── reporter.py
│ ├── runner.py
│ ├── file_coverage.py
│ ├── formatter.py
│ └── ci.py
├── __init__.py
└── __main__.py
├── .dockerignore
├── requirements.txt
├── setup.cfg
├── bin
├── post-release
├── test-release
├── release
└── prep-release
├── .pypirc.sample
├── tox.ini
├── circle.yml
├── Dockerfile
├── .gitignore
├── .codeclimate.yml
├── Makefile
├── setup.py
├── LICENSE.txt
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import *
2 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/VERSION:
--------------------------------------------------------------------------------
1 | 0.2.3
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/*.pyc
2 | .git
3 | .coverage
4 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/__init__.py:
--------------------------------------------------------------------------------
1 | from . import *
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | coverage>=4.0,<4.4
3 | pytest-cov
4 | pytest
5 | HTTPretty
6 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 | [pep8]
5 | max-line-length = 100
6 |
--------------------------------------------------------------------------------
/tests/fixtures/latin_1_source.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qltysh-archive/python-test-reporter/HEAD/tests/fixtures/latin_1_source.py
--------------------------------------------------------------------------------
/tests/fixtures/coverage.txt:
--------------------------------------------------------------------------------
1 | !coverage.py: This is a private format, don't read it directly!{"lines": {"source.py": [4, 5, 6, 7, 9, 10, 12, 15, 16]}}
2 |
--------------------------------------------------------------------------------
/tests/fixtures/coverage_invalid_version.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/bin/post-release:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Usage: bin/post-release
4 | #
5 | ###
6 | set -e
7 |
8 | git tag -f v$(cat codeclimate_test_reporter/VERSION)
9 | git push origin --tags
10 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | codeclimate-test-reporter
3 | """
4 |
5 | import os
6 |
7 | __author__ = "Code Climate"
8 | __version__ = open(os.path.join(os.path.dirname(__file__), "VERSION")).read().strip()
9 | __license__ = "MIT"
10 |
--------------------------------------------------------------------------------
/bin/test-release:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Test release a new version of this repository.
4 | #
5 | # Usage: bin/test-release
6 | #
7 | ###
8 |
9 | python setup.py build
10 | python setup.py register -r pypitest
11 | python setup.py sdist upload -r pypitest
12 |
--------------------------------------------------------------------------------
/.pypirc.sample:
--------------------------------------------------------------------------------
1 | [distutils]
2 | index-servers =
3 | pypi
4 | pypitest
5 |
6 | [pypi]
7 | repository=https://pypi.python.org/pypi
8 | username=
9 | password=
10 |
11 | [pypitest]
12 | repository=https://testpypi.python.org/pypi
13 | username=
14 | password=
15 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import sys
4 |
5 | from .components.runner import Runner
6 |
7 |
8 | def run():
9 | runner = Runner()
10 |
11 | sys.exit(runner.run())
12 |
13 | if __name__ == '__main__':
14 | run()
15 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist=py27,py34,py35
3 |
4 | [testenv]
5 | commands=python setup.py test
6 | deps=-rrequirements.txt
7 |
8 | [testenv:py35]
9 | commands=python setup.py testcov
10 | python -m codeclimate_test_reporter
11 | passenv=CODECLIMATE_REPO_TOKEN
12 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | environment:
3 | CODECLIMATE_REPO_TOKEN: 783d26fa43e7bee1cc8791677e808941795bc20f6d530e92905e2d8577a2de06
4 |
5 | dependencies:
6 | override:
7 | - pip install tox tox-pyenv
8 | - pyenv local 2.7.11 3.4.4 3.5.1
9 |
10 | test:
11 | override:
12 | - tox
13 |
--------------------------------------------------------------------------------
/bin/release:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Release a new version of this repository.
4 | #
5 | # Assumes bin/prep-release was run and the PR merged.
6 | #
7 | # Usage: bin/release
8 | #
9 | ###
10 | set -e
11 |
12 | python setup.py build
13 | python setup.py register -r pypi
14 | python setup.py sdist upload -r pypi
15 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.5.1-alpine
2 |
3 | WORKDIR /usr/src/app
4 |
5 | RUN apk --update add git
6 |
7 | COPY requirements.txt /usr/src/app/
8 | RUN pip install --upgrade pip && \
9 | pip install -r requirements.txt
10 |
11 | COPY . /usr/src/app
12 |
13 | RUN adduser -u 9000 -D app
14 | RUN chown -R app:app /usr/src/app
15 | USER app
16 |
17 | ENTRYPOINT ["/usr/local/bin/python", "-m", "codeclimate_test_reporter"]
18 |
--------------------------------------------------------------------------------
/tests/fixtures/source.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 |
4 | class Person:
5 | def __init__(self, first_name, last_name):
6 | self.first_name = first_name
7 | self.last_name = last_name
8 |
9 | def fullname(self):
10 | return "%s %s" % (self.first_name, self.last_name)
11 |
12 | def not_called(self):
13 | print("Shouldn't be called")
14 |
15 | person = Person("Marty", "McFly")
16 | person.fullname()
17 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import httpretty
3 |
4 |
5 | class ApiMock:
6 | def setup(self, status_code):
7 | httpretty.enable()
8 | httpretty.register_uri(
9 | httpretty.POST,
10 | "http://example.com/test_reports",
11 | status=status_code,
12 | content_type="application/plain"
13 | )
14 |
15 | def cleanup(self):
16 | httpretty.disable()
17 | httpretty.reset()
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # Installer logs
28 | pip-log.txt
29 | pip-delete-this-directory.txt
30 |
31 | # Unit test / coverage reports
32 | htmlcov/
33 | .tox/
34 | .coverage
35 | .coverage.*
36 | .cache
37 | nosetests.xml
38 | coverage.xml
39 | *,cover
40 | .hypothesis/
41 |
42 | # dotenv
43 | .env
44 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/git_command.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 |
4 |
5 | class GitCommand:
6 | def branch(self):
7 | return self.__execute("git rev-parse --abbrev-ref HEAD")
8 |
9 | def committed_at(self):
10 | return self.__execute("git log -1 --pretty=format:%ct")
11 |
12 | def head(self):
13 | return self.__execute("git log -1 --pretty=format:'%H'")
14 |
15 | def __execute(self, command):
16 | process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
17 | exit_code = process.wait()
18 |
19 | if exit_code == 0:
20 | return process.stdout.read().decode("utf-8").strip()
21 | else:
22 | return None
23 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 | engines:
3 | duplication:
4 | enabled: true
5 | config:
6 | languages:
7 | - python
8 | exclude_paths:
9 | # duplicate structures in dictionary
10 | - codeclimate_test_reporter/components/ci.py
11 | fixme:
12 | enabled: true
13 | markdownlint:
14 | enabled: true
15 | pep8:
16 | enabled: true
17 | radon:
18 | enabled: true
19 | exclude_fingerprints:
20 | # complexity in PayloadValidator#validate
21 | - 1b18d0fde7d0511be196bcc366180bf8
22 | # complexity in Runner#run
23 | - c1a982077efa39ca50db944490e15485
24 | ratings:
25 | paths:
26 | - "**.inc"
27 | - "**.module"
28 | - "**.py"
29 | exclude_paths:
30 | - dist/
31 | - tests/
32 | - build/
33 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/api_client.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import requests
4 |
5 |
6 | class ApiClient:
7 | def __init__(self, host=None, timeout=5):
8 | self.host = host or self.__default_host().rstrip("/")
9 | self.timeout = timeout
10 |
11 | def post(self, payload):
12 | headers = {"Content-Type": "application/json"}
13 | response = requests.post(
14 | "%s/test_reports" % self.host,
15 | data=json.dumps(payload),
16 | headers=headers,
17 | timeout=self.timeout
18 | )
19 |
20 | return response
21 |
22 | def __default_host(self):
23 | return os.environ.get("CODECLIMATE_API_HOST", "https://codeclimate.com")
24 |
--------------------------------------------------------------------------------
/tests/test_argument_parser.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from codeclimate_test_reporter.components.argument_parser import ArgumentParser
4 |
5 |
6 | def test_parse_args_default():
7 | parsed_args = ArgumentParser().parse_args([])
8 |
9 | assert(parsed_args.file == "./.coverage")
10 | assert(parsed_args.token is None)
11 | assert(parsed_args.stdout is False)
12 | assert(parsed_args.debug is False)
13 | assert(parsed_args.version is False)
14 |
15 | def test_parse_args_with_options():
16 | args = ["--version", "--debug", "--stdout", "--file", "file", "--token", "token"]
17 | parsed_args = ArgumentParser().parse_args(args)
18 |
19 | assert(parsed_args.debug)
20 | assert(parsed_args.file == "file")
21 | assert(parsed_args.token == "token")
22 | assert(parsed_args.stdout)
23 | assert(parsed_args.version)
24 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all clean-pyc image test test-release release run
2 |
3 | IMAGE_NAME ?= codeclimate/python-test-reporter
4 |
5 | all: image
6 |
7 | clean-pyc:
8 | find . -name '*.pyc' -exec rm -f {} +
9 | find . -name '*.pyo' -exec rm -f {} +
10 | find . -name '*~' -exec rm -f {} +
11 |
12 | image:
13 | docker build --tag $(IMAGE_NAME) .
14 |
15 | test: image
16 | docker run \
17 | -it \
18 | --rm \
19 | --entrypoint=/bin/sh \
20 | $(IMAGE_NAME) -c 'python setup.py testcov'
21 |
22 | test-release: image
23 | docker run \
24 | --rm \
25 | --volume ~/.pypirc:/home/app/.pypirc \
26 | --entrypoint=/bin/sh \
27 | $(IMAGE_NAME) -c 'bin/test-release'
28 |
29 | release: image
30 | docker run \
31 | --rm \
32 | --volume ~/.pypirc:/home/app/.pypirc \
33 | --entrypoint=/bin/sh \
34 | $(IMAGE_NAME) -c 'bin/release' && bin/post-release
35 |
36 | run: image
37 | docker run --rm $(IMAGE_NAME) --version
38 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/argument_parser.py:
--------------------------------------------------------------------------------
1 | """
2 | CLI arguments definition.
3 | """
4 |
5 | import argparse
6 | import sys
7 |
8 |
9 | class ArgumentParser(argparse.ArgumentParser):
10 | def __init__(self):
11 | argparse.ArgumentParser.__init__(
12 | self,
13 | prog="codeclimate-test-reporter",
14 | description="Report test coverage to Code Climate"
15 | )
16 |
17 | self.add_argument(
18 | "--file",
19 | help="A coverage.py coverage file to report",
20 | default="./.coverage"
21 | )
22 |
23 | self.add_argument("--token", help="Code Climate repo token")
24 | self.add_argument("--stdout", help="Output to STDOUT", action="store_true")
25 | self.add_argument("--debug", help="Enable debug mode", action="store_true")
26 | self.add_argument("--version", help="Show the version", action="store_true")
27 |
--------------------------------------------------------------------------------
/tests/test_api_client.py:
--------------------------------------------------------------------------------
1 | import httpretty
2 | import os
3 | import pytest
4 |
5 | from codeclimate_test_reporter.components.api_client import ApiClient
6 |
7 |
8 | def test_post():
9 | httpretty.enable()
10 | httpretty.register_uri(
11 | httpretty.POST,
12 | "http://example.com/test_reports",
13 | status=200,
14 | body="ok",
15 | content_type="application/plain"
16 | )
17 |
18 | client = ApiClient(host="http://example.com")
19 | response = client.post({})
20 |
21 | assert(response.status_code == 200)
22 |
23 | httpretty.disable()
24 | httpretty.reset()
25 |
26 | def test_env_host():
27 | os.environ["CODECLIMATE_API_HOST"] = "http://example.com"
28 |
29 | client = ApiClient()
30 |
31 | assert(client.host == "http://example.com")
32 |
33 | del os.environ["CODECLIMATE_API_HOST"]
34 |
35 | def test_default_host():
36 | client = ApiClient()
37 |
38 | assert(client.host == "https://codeclimate.com")
39 |
--------------------------------------------------------------------------------
/tests/fixtures/coverage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/test_reporter.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import shutil
4 | import subprocess
5 |
6 | from codeclimate_test_reporter.components.reporter import Reporter
7 | from codeclimate_test_reporter.components.argument_parser import ArgumentParser
8 | from .utils import ApiMock
9 |
10 |
11 | def test_run():
12 | os.environ["CODECLIMATE_API_HOST"] = "http://example.com"
13 |
14 | api_mock = ApiMock()
15 | api_mock.setup(200)
16 |
17 | parsed_args = ArgumentParser().parse_args(["--file", "./coverage.txt", "--token", "token"])
18 | reporter = Reporter(parsed_args)
19 |
20 | orig_dir = os.getcwd()
21 | os.chdir("./tests/fixtures")
22 |
23 | subprocess.call(["git", "init"])
24 | subprocess.call(["git", "config", "user.name", "Test User"])
25 | subprocess.call(["git", "config", "user.email", "test@example.com"])
26 | subprocess.call(["git", "commit", "--allow-empty", "--message", "init"])
27 |
28 | try:
29 | return_code = reporter.run()
30 |
31 | assert(return_code == 0)
32 | finally:
33 | del os.environ["CODECLIMATE_API_HOST"]
34 | os.chdir(orig_dir)
35 | shutil.rmtree("./tests/fixtures/.git")
36 |
--------------------------------------------------------------------------------
/bin/prep-release:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Open a PR for releasing a new version of this repository.
4 | #
5 | # Usage: bin/prep-release VERSION
6 | #
7 | ###
8 | set -e
9 |
10 | if [ -z "$1" ]; then
11 | echo "usage: bin/prep-release VERSION" >&2
12 | exit 64
13 | fi
14 |
15 | version=$1
16 | old_version=$(< codeclimate_test_reporter/VERSION)
17 | branch="release-$version"
18 |
19 | if ! make test; then
20 | echo "test failure, not releasing" >&2
21 | exit 1
22 | fi
23 |
24 | printf "RELEASE %s => %s\n" "$old_version" "$version"
25 | git checkout master
26 | git pull origin master
27 |
28 | git checkout -b "$branch"
29 |
30 | printf "%s\n" "$version" > codeclimate_test_reporter/VERSION
31 | git add codeclimate_test_reporter/VERSION
32 | git commit -m "Release v$version"
33 | git push origin "$branch"
34 |
35 | branch_head=$(git rev-parse --short $branch)
36 | if command -v hub > /dev/null 2>&1; then
37 | hub pull-request -F - <&2
44 | fi
45 |
46 | echo "After merging the version-bump PR, run: make release"
47 |
--------------------------------------------------------------------------------
/tests/fixtures/coverage_for_latin_1_source.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | /home/ulysse/Documents/programmes/python-test-reporter/tests/fixtures
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import Command, find_packages, setup
2 | from subprocess import call
3 |
4 | from codeclimate_test_reporter.__init__ import __version__ as reporter_version
5 |
6 |
7 | class RunTests(Command):
8 | description = "Run tests"
9 | user_options = []
10 |
11 | def initialize_options(self):
12 | pass
13 |
14 | def finalize_options(self):
15 | pass
16 |
17 | def run(self):
18 | errno = call(["py.test", "tests/"])
19 | raise SystemExit(errno)
20 |
21 |
22 | class RunTestsCov(RunTests):
23 | description = "Run tests w/ coverage"
24 |
25 | def run(self):
26 | """Run all tests with coverage!"""
27 | errno = call(["py.test", "--cov=codeclimate_test_reporter", "tests/"])
28 | raise SystemExit(errno)
29 |
30 |
31 | setup(
32 | name="codeclimate-test-reporter",
33 | version=reporter_version,
34 | description="Report test coverage to Code Climate",
35 | url="http://github.com/codeclimate/python-test-reporter",
36 | author="Code Climate",
37 | author_email="hello@codeclimate.com",
38 | maintainer="Code Climate",
39 | maintainer_email="hello@codeclimate.com",
40 | license="MIT",
41 | packages=find_packages(exclude=["tests"]),
42 | zip_safe=False,
43 | cmdclass={"test": RunTests, "testcov": RunTestsCov},
44 | entry_points={
45 | "console_scripts": [
46 | "codeclimate-test-reporter=codeclimate_test_reporter.__main__:run",
47 | ],
48 | },
49 | package_data={"codeclimate_test_reporter": ["VERSION"]},
50 | install_requires=["coverage>=4.0", "requests"],
51 | )
52 |
--------------------------------------------------------------------------------
/tests/test_ci.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from codeclimate_test_reporter.components.ci import CI
4 |
5 |
6 | def test_travis_data():
7 | env = {
8 | "TRAVIS": True,
9 | "TRAVIS_BRANCH": "master",
10 | "TRAVIS_JOB_ID": "4.1",
11 | "TRAVIS_PULL_REQUEST": "false"
12 | }
13 |
14 | expected_data = {
15 | "name": "travis-ci",
16 | "branch": env["TRAVIS_BRANCH"],
17 | "build_identifier": env["TRAVIS_JOB_ID"],
18 | "pull_request": env["TRAVIS_PULL_REQUEST"]
19 | }
20 |
21 | data = CI(env).data()
22 |
23 | assert data == expected_data
24 |
25 | def test_circle_data():
26 | env = {
27 | "CIRCLECI": True,
28 | "CIRCLE_BRANCH": "master",
29 | "CIRCLE_BUILD_NUM": "123",
30 | "CIRCLE_SHA1": "7638417db6d59f3c431d3e1f261cc637155684cd"
31 | }
32 |
33 | expected_data = {
34 | "name": "circleci",
35 | "branch": env["CIRCLE_BRANCH"],
36 | "build_identifier": env["CIRCLE_BUILD_NUM"],
37 | "commit_sha": env["CIRCLE_SHA1"]
38 | }
39 |
40 | data = CI(env).data()
41 |
42 | assert data == expected_data
43 |
44 | def test_ci_pick():
45 | assert CI({ "TRAVIS": True }).data()["name"] == "travis-ci"
46 | assert CI({ "CIRCLECI": True }).data()["name"] == "circleci"
47 | assert CI({ "SEMAPHORE": True }).data()["name"] == "semaphore"
48 | assert CI({ "JENKINS_URL": True }).data()["name"] == "jenkins"
49 | assert CI({ "TDDIUM": True }).data()["name"] == "tddium"
50 | assert CI({ "WERCKER": True }).data()["name"] == "wercker"
51 | assert CI({ "APPVEYOR": True }).data()["name"] == "appveyor"
52 | assert CI({ "CI_NAME": "DRONE" }).data()["name"] == "drone"
53 | assert CI({ "CI_NAME": "CODESHIP" }).data()["name"] == "codeship"
54 | assert CI({ "CI_NAME": "VEXOR" }).data()["name"] == "vexor"
55 | assert CI({ "BUILDKITE": True }).data()["name"] == "buildkite"
56 | assert CI({ "GITLAB_CI": True }).data()["name"] == "gitlab-ci"
57 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/payload_validator.py:
--------------------------------------------------------------------------------
1 | class InvalidPayload(Exception):
2 | pass
3 |
4 |
5 | class PayloadValidator:
6 | def __init__(self, payload):
7 | self.payload = payload
8 |
9 | def validate(self):
10 | if not self.__commit_sha():
11 | raise InvalidPayload("A git commit sha was not found in the test report payload")
12 | elif not self.__committed_at():
13 | raise InvalidPayload("A git commit timestamp was not found in the test report payload")
14 | elif not self.__run_at():
15 | raise InvalidPayload("A run at timestamp was not found in the test report payload")
16 | elif not self.__source_files():
17 | raise InvalidPayload("No source files were found in the test report payload")
18 | elif not self.__valid_source_files():
19 | raise InvalidPayload("Invalid source files were found in the test report payload")
20 | else:
21 | return True
22 |
23 | def __commit_sha(self):
24 | return self.__commit_sha_from_git() or self.__commit_sha_from_ci_service()
25 |
26 | def __commit_sha_from_git(self):
27 | return self.__validate_payload_value(["git", "head"])
28 |
29 | def __commit_sha_from_ci_service(self):
30 | return self.__validate_payload_value(["ci_service", "commit_sha"])
31 |
32 | def __committed_at(self):
33 | return self.__validate_payload_value(["git", "committed_at"])
34 |
35 | def __run_at(self):
36 | return self.payload.get("run_at")
37 |
38 | def __source_files(self):
39 | return self.payload.get("source_files")
40 |
41 | def __validate_payload_value(self, keys):
42 | current = self.payload
43 |
44 | for key in keys:
45 | next = current.get(key)
46 |
47 | if next:
48 | current = next
49 | else:
50 | return False
51 |
52 | return True
53 |
54 | def __valid_source_files(self):
55 | return all(self.__valid_source_file(source_file) for source_file in self.__source_files())
56 |
57 | def __valid_source_file(self, source_file):
58 | return type(source_file) is dict and source_file.get("name") and source_file.get("coverage")
59 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/reporter.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | import coverage as Coverage
4 | import json
5 | import os
6 | import sys
7 |
8 | from ..components.api_client import ApiClient
9 | from ..components.formatter import Formatter
10 | from ..components.payload_validator import PayloadValidator
11 |
12 |
13 | class CoverageFileNotFound(Exception):
14 | pass
15 |
16 |
17 | class Reporter:
18 | def __init__(self, args):
19 | self.args = args
20 |
21 | def run(self):
22 | """
23 | The main program without error handling
24 |
25 | :param args: parsed args (argparse.Namespace)
26 | :return: status code
27 |
28 | """
29 |
30 | if not os.path.isfile(self.args.file):
31 | message = "Coverage file `" + self.args.file + "` file not found. "
32 | raise CoverageFileNotFound(message)
33 |
34 | xml_filepath = self.__create_xml_report(self.args.file)
35 | formatter = Formatter(xml_filepath, self.args.debug)
36 | payload = formatter.payload()
37 |
38 | PayloadValidator(payload).validate()
39 |
40 | if self.args.stdout:
41 | print(json.dumps(payload))
42 |
43 | return 0
44 | else:
45 | client = ApiClient()
46 |
47 | print("Submitting payload to %s... " % client.host, end="")
48 | sys.stdout.flush()
49 |
50 | response = self.__post_payload(client, payload)
51 |
52 | print("done!")
53 |
54 | return response
55 |
56 | def __post_payload(self, client, payload):
57 | payload["repo_token"] = self.args.token or os.environ.get("CODECLIMATE_REPO_TOKEN")
58 |
59 | if payload["repo_token"]:
60 | response = client.post(payload)
61 | response.raise_for_status()
62 |
63 | return 0
64 | else:
65 | print("CODECLIMATE_REPO_TOKEN is required and not set")
66 |
67 | return 1
68 |
69 | def __create_xml_report(self, file):
70 | cov = Coverage.coverage(file)
71 | cov.load()
72 | data = cov.get_data()
73 |
74 | xml_filepath = "/tmp/coverage.xml"
75 | cov.xml_report(outfile=xml_filepath)
76 |
77 | return xml_filepath
78 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Code Climate LLC
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 |
21 | This gem includes code by Wil Gieseler, distributed under the MIT license:
22 |
23 | Permission is hereby granted, free of charge, to any person obtaining
24 | a copy of this software and associated documentation files (the
25 | "Software"), to deal in the Software without restriction, including
26 | without limitation the rights to use, copy, modify, merge, publish,
27 | distribute, sublicense, and/or sell copies of the Software, and to
28 | permit persons to whom the Software is furnished to do so, subject to
29 | the following conditions:
30 |
31 | The above copyright notice and this permission notice shall be
32 | included in all copies or substantial portions of the Software.
33 |
34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
35 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
36 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
37 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
38 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
39 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 |
--------------------------------------------------------------------------------
/tests/test_runner.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import shutil
4 | import subprocess
5 | import sys
6 |
7 | if sys.version_info >= (3, 0):
8 | from io import StringIO
9 | else:
10 | from StringIO import StringIO
11 |
12 | from codeclimate_test_reporter.components.runner import Runner
13 | from codeclimate_test_reporter.__init__ import __version__ as reporter_version
14 | from .utils import ApiMock
15 |
16 |
17 | def test_version():
18 | out = StringIO()
19 | runner = Runner(["--version"], out=out)
20 |
21 | return_code = runner.run()
22 |
23 | assert(return_code == 0)
24 | assert(out.getvalue().strip() == reporter_version)
25 |
26 | def test_run():
27 | os.environ["CODECLIMATE_API_HOST"] = "http://example.com"
28 |
29 | api_mock = ApiMock()
30 | api_mock.setup(200)
31 |
32 | runner = Runner(["--file", "./coverage.txt", "--token", "token"])
33 |
34 | orig_dir = os.getcwd()
35 | os.chdir("./tests/fixtures")
36 |
37 | subprocess.call(["git", "init"])
38 | subprocess.call(["git", "config", "user.name", "Test User"])
39 | subprocess.call(["git", "config", "user.email", "test@example.com"])
40 | subprocess.call(["git", "commit", "--allow-empty", "--message", "init"])
41 |
42 | try:
43 | return_code = runner.run()
44 |
45 | assert(return_code == 0)
46 | finally:
47 | del os.environ["CODECLIMATE_API_HOST"]
48 | os.chdir(orig_dir)
49 | shutil.rmtree("./tests/fixtures/.git")
50 | api_mock.cleanup()
51 |
52 | def test_run_api_500_error():
53 | os.environ["CODECLIMATE_API_HOST"] = "http://example.com"
54 |
55 | api_mock = ApiMock()
56 | api_mock.setup(500)
57 |
58 | err = StringIO()
59 | runner = Runner(["--file", "./coverage.txt", "--token", "token"], err=err)
60 |
61 | orig_dir = os.getcwd()
62 | os.chdir("./tests/fixtures")
63 |
64 | subprocess.call(["git", "init"])
65 | subprocess.call(["git", "config", "user.name", "Test User"])
66 | subprocess.call(["git", "config", "user.email", "test@example.com"])
67 | subprocess.call(["git", "commit", "--allow-empty", "--message", "init"])
68 |
69 | try:
70 | return_code = runner.run()
71 |
72 | assert(return_code == 1)
73 | assert("500 Server Error" in err.getvalue().strip())
74 | finally:
75 | del os.environ["CODECLIMATE_API_HOST"]
76 | os.chdir(orig_dir)
77 | shutil.rmtree("./tests/fixtures/.git")
78 | api_mock.cleanup()
79 |
--------------------------------------------------------------------------------
/tests/test_formatter.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | import shutil
4 | import subprocess
5 | import unittest
6 |
7 | from codeclimate_test_reporter.components.formatter import Formatter
8 | from codeclimate_test_reporter.components.formatter import InvalidReportVersion
9 | from codeclimate_test_reporter.components.payload_validator import PayloadValidator
10 |
11 |
12 | class FormatterTest(unittest.TestCase):
13 | def test_payload_latin_1(self):
14 | orig_dir = os.getcwd()
15 | os.chdir("./tests/fixtures")
16 |
17 | self.__setup_git()
18 |
19 | try:
20 | formatter = Formatter("coverage_for_latin_1_source.xml")
21 | payload = formatter.payload()
22 | finally:
23 | os.chdir(orig_dir)
24 | shutil.rmtree("./tests/fixtures/.git")
25 |
26 | source_file = payload["source_files"][0]
27 | expected_line_counts = {"covered": 9, "missed": 1, "total": 10}
28 | expected_coverage = "[null, null, null, null, 1, 1, 1, 1, null, 1, 1, null, 1, 0, null, 1, 1]"
29 |
30 | assert type(payload) is dict
31 | assert len(payload["source_files"]) == 1
32 |
33 | assert source_file["line_counts"] == expected_line_counts
34 | assert source_file["covered_percent"] == 0.9
35 | assert source_file["covered_strength"] == 1.0
36 | assert source_file["coverage"] == expected_coverage
37 |
38 | assert PayloadValidator(payload).validate()
39 |
40 | def test_payload(self):
41 | orig_dir = os.getcwd()
42 | os.chdir("./tests/fixtures")
43 |
44 | self.__setup_git()
45 |
46 | try:
47 | formatter = Formatter("coverage.xml")
48 | payload = formatter.payload()
49 | finally:
50 | os.chdir(orig_dir)
51 | shutil.rmtree("./tests/fixtures/.git")
52 |
53 | assert type(payload) is dict
54 | assert len(payload["source_files"]) == 1
55 |
56 | source_file = payload["source_files"][0]
57 | expected_line_counts = {"covered": 9, "missed": 1, "total": 10}
58 | expected_coverage = "[null, null, null, 1, 1, 1, 1, null, 1, 1, null, 1, 0, null, 1, 1]"
59 |
60 | assert source_file["line_counts"] == expected_line_counts
61 | assert source_file["covered_percent"] == 0.9
62 | assert source_file["covered_strength"] == 1.0
63 | assert source_file["coverage"] == expected_coverage
64 |
65 | assert PayloadValidator(payload).validate()
66 |
67 | def test_payload_incompatible_version(self):
68 | orig_dir = os.getcwd()
69 | os.chdir("./tests/fixtures")
70 |
71 | try:
72 | formatter = Formatter("coverage_invalid_version.xml")
73 | self.assertRaises(InvalidReportVersion, formatter.payload)
74 | finally:
75 | os.chdir(orig_dir)
76 |
77 | def __setup_git(self):
78 | subprocess.call(["git", "init"])
79 | subprocess.call(["git", "config", "user.name", "Test User"])
80 | subprocess.call(["git", "config", "user.email", "test@example.com"])
81 | subprocess.call(["git", "commit", "--allow-empty", "--message", "init"])
82 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/runner.py:
--------------------------------------------------------------------------------
1 | """This module provides the main functionality of codeclimate-test-reporter.
2 |
3 | Invocation flow:
4 |
5 | 1. Read, validate and process the input (args, `stdin`).
6 | 2. Create and send test coverage to codeclimate.com.
7 | 3. Report back to `stdout`.
8 | 4. Exit.
9 |
10 | """
11 | from coverage.misc import CoverageException
12 | import os
13 | import platform
14 | import sys
15 | import requests.exceptions
16 |
17 | from ..__init__ import __version__ as reporter_version
18 | from ..components.argument_parser import ArgumentParser
19 | from ..components.formatter import InvalidReportVersion
20 | from ..components.payload_validator import InvalidPayload
21 | from ..components.reporter import CoverageFileNotFound, Reporter
22 |
23 |
24 | class Runner:
25 | def __init__(self, args=sys.argv[1:], out=sys.stdout, err=sys.stderr):
26 | self.parsed_args = ArgumentParser().parse_args(args)
27 | self.out = out
28 | self.err = err
29 |
30 | def run(self):
31 | """
32 | The main function.
33 |
34 | Pre-process args, handle some special types of invocations,
35 | and run the main program with error handling.
36 |
37 | Return exit status code.
38 |
39 | """
40 |
41 | if self.parsed_args.version:
42 | self.out.write(reporter_version)
43 | return 0
44 |
45 | if self.parsed_args.debug:
46 | self.out.write(self.__debug_info())
47 |
48 | try:
49 | reporter = Reporter(self.parsed_args)
50 | exit_status = reporter.run()
51 | return exit_status
52 | except CoverageFileNotFound as e:
53 | return self.__handle_error(
54 | str(e) + "\nUse --file to specifiy an alternate location."
55 | )
56 | except CoverageException as e:
57 | return self.__handle_error(str(e), support=True)
58 | except InvalidPayload as e:
59 | return self.__handle_error("Invalid Payload: " + str(e), support=True)
60 | except InvalidReportVersion as e:
61 | return self.__handle_error(str(e))
62 | except requests.exceptions.HTTPError as e:
63 | return self.__handle_error(str(e), support=True)
64 | except requests.exceptions.Timeout:
65 | return self.__handle_error(
66 | "Client HTTP Timeout: No response in 5 seconds.",
67 | support=True
68 | )
69 |
70 | def __handle_error(self, message, support=False):
71 | self.err.write(message)
72 |
73 | if support:
74 | self.err.write(
75 | "\n\nContact support at https://codeclimate.com/help "
76 | "with the following debug info if error persists:"
77 | "\n" + message + "\n" + self.__debug_info()
78 | )
79 |
80 | return 1
81 |
82 | def __debug_info(self):
83 | from requests import __version__ as requests_version
84 |
85 | return "\n".join([
86 | "codeclimate-test-repoter %s" % reporter_version,
87 | "Requests %s" % requests_version,
88 | "Python %s\n%s" % (sys.version, sys.executable),
89 | "%s %s" % (platform.system(), platform.release()),
90 | ])
91 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/file_coverage.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 | from hashlib import sha1
4 |
5 | if sys.version_info < (3, 0):
6 | from io import open
7 |
8 |
9 | def read_file_content(path):
10 | """
11 | Read a file content, either if it is utf-8 or latin-1 encoding
12 | :param path: path to the file
13 | :return: file content
14 | """
15 | try:
16 | return open(path, "r", encoding="utf-8-sig").read()
17 | except UnicodeDecodeError:
18 | return open(path, "r", encoding="iso-8859-1").read()
19 |
20 |
21 | class FileCoverage:
22 | def __init__(self, file_node):
23 | self.file_body = None
24 | self.file_node = file_node
25 | self.__process()
26 |
27 | def payload(self):
28 | return {
29 | "name": self.__filename(),
30 | "blob_id": self.__blob(),
31 | "covered_strength": self.__covered_strength(),
32 | "covered_percent": self.__covered_percent(),
33 | "coverage": json.dumps(self.__coverage()),
34 | "line_counts": self.__line_counts()
35 | }
36 |
37 | def __process(self):
38 | self.total = len(self.__line_nodes())
39 | self.hits = 0
40 | self.covered = 0
41 | self.missed = 0
42 |
43 | for line_node in self.__line_nodes():
44 | hits = int(line_node.get("hits"))
45 |
46 | if hits > 0:
47 | self.covered += 1
48 | self.hits += hits
49 | else:
50 | self.missed += 1
51 |
52 | def __line_nodes(self):
53 | return self.file_node.findall("lines/line")
54 |
55 | def __blob(self):
56 | contents = self.__file_body()
57 | header = "blob " + str(len(contents)) + "\0"
58 |
59 | return sha1((header + contents).encode("utf-8")).hexdigest()
60 |
61 | def __file_body(self):
62 | if not self.file_body:
63 | self.file_body = read_file_content(self.__filename())
64 |
65 | return self.file_body
66 |
67 | def __filename(self):
68 | return self.file_node.get("filename")
69 |
70 | def __rate(self):
71 | return self.file_node.get("line-rate")
72 |
73 | def __covered_strength(self):
74 | return self.__guard_division(self.hits, self.covered)
75 |
76 | def __num_lines(self):
77 | return len(self.__file_body().splitlines())
78 |
79 | def __covered_percent(self):
80 | return self.__guard_division(self.covered, self.total)
81 |
82 | def __guard_division(self, dividend, divisor):
83 | if (divisor > 0):
84 | return dividend / float(divisor)
85 | else:
86 | return 0.0
87 |
88 | def __coverage(self):
89 | coverage = [None] * self.__num_lines()
90 |
91 | for line_node in self.__line_nodes():
92 | index = int(line_node.get("number")) - 1
93 | hits = int(line_node.get("hits"))
94 |
95 | coverage[index] = hits
96 |
97 | return coverage
98 |
99 | def __line_counts(self):
100 | return {
101 | "total": self.total,
102 | "covered": self.covered,
103 | "missed": self.missed
104 | }
105 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/formatter.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as ET
2 |
3 | from ..__init__ import __version__ as reporter_version
4 | from .ci import CI
5 | from .file_coverage import FileCoverage
6 | from .git_command import GitCommand
7 |
8 |
9 | class InvalidReportVersion(Exception):
10 | pass
11 |
12 |
13 | class Formatter:
14 | def __init__(self, xml_report_path, debug=False):
15 | self.debug = debug
16 | tree = ET.parse(xml_report_path)
17 | self.root = tree.getroot()
18 |
19 | def payload(self):
20 | self.__validate_report_version()
21 |
22 | total_line_counts = {"covered": 0, "missed": 0, "total": 0}
23 | total_covered_strength = 0.0
24 | total_covered_percent = 0.0
25 |
26 | source_files = self.__source_files()
27 |
28 | for source_file in source_files:
29 | total_covered_strength += source_file["covered_strength"]
30 | total_covered_percent += source_file["covered_percent"]
31 |
32 | for key, value in source_file["line_counts"].items():
33 | total_line_counts[key] += value
34 |
35 | total_covered_strength = round(total_covered_strength / len(source_files), 2)
36 | total_covered_percent = round(total_covered_percent / len(source_files), 2)
37 |
38 | return {
39 | "run_at": self.__timestamp(),
40 | "covered_percent": total_covered_percent,
41 | "covered_strength": total_covered_strength,
42 | "line_counts": total_line_counts,
43 | "partial": False,
44 | "git": self.__git_info(),
45 | "environment": {
46 | "pwd": self.root.find("sources").find("source").text,
47 | "reporter_version": reporter_version
48 | },
49 | "ci_service": self.__ci_data(),
50 | "source_files": source_files
51 | }
52 |
53 | def __validate_report_version(self):
54 | report_version = self.root.get("version")
55 |
56 | if report_version < "4.0" or report_version >= "4.4":
57 | message = """
58 | This reporter is compatible with Coverage.py versions >=4.0,<4.4.
59 | Your Coverage.py report version is %s.
60 |
61 | Consider locking your version of Coverage.py to >4,0,<4.4 until the
62 | following Coverage.py issue is addressed:
63 | https://bitbucket.org/ned/coveragepy/issues/578/incomplete-file-path-in-xml-report
64 | """ % (report_version)
65 |
66 | raise InvalidReportVersion(message)
67 |
68 | def __ci_data(self):
69 | return CI().data()
70 |
71 | def __source_files(self):
72 | source_files = []
73 |
74 | for file_node in self.__file_nodes():
75 | if self.debug:
76 | print("Processing file path=%s" % file_node.get("filename"))
77 |
78 | file_coverage = FileCoverage(file_node)
79 | payload = file_coverage.payload()
80 | source_files.append(payload)
81 |
82 | return source_files
83 |
84 | def __file_nodes(self):
85 | return self.root.findall("packages/package/classes/class")
86 |
87 | def __timestamp(self):
88 | return self.root.get("timestamp")
89 |
90 | def __git_info(self):
91 | ci_data = self.__ci_data()
92 | command = GitCommand()
93 |
94 | return {
95 | "branch": ci_data.get("branch") or command.branch(),
96 | "committed_at": command.committed_at(),
97 | "head": ci_data.get("commit_sha") or command.head()
98 | }
99 |
--------------------------------------------------------------------------------
/codeclimate_test_reporter/components/ci.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 |
4 | import itertools
5 |
6 |
7 | class CI:
8 | def __init__(self, env=os.environ):
9 | self.env = env
10 |
11 | def predicate(self, service):
12 | return service["matcher"](self.env)
13 |
14 | def data(self):
15 | service = next(iter(filter(self.predicate, self.__services())), None)
16 |
17 | if service:
18 | return service["data"](self.env)
19 | else:
20 | return {}
21 |
22 | def __services(self):
23 | return [{
24 | "matcher": lambda env: env.get("TRAVIS"),
25 | "data": lambda env: {
26 | "name": "travis-ci",
27 | "branch": self.env.get("TRAVIS_BRANCH"),
28 | "build_identifier": self.env.get("TRAVIS_JOB_ID"),
29 | "pull_request": self.env.get("TRAVIS_PULL_REQUEST")
30 | }
31 | }, {
32 | "matcher": lambda env: env.get("CIRCLECI"),
33 | "data": lambda env: {
34 | "name": "circleci",
35 | "branch": self.env.get("CIRCLE_BRANCH"),
36 | "build_identifier": self.env.get("CIRCLE_BUILD_NUM"),
37 | "commit_sha": self.env.get("CIRCLE_SHA1")
38 | }
39 | }, {
40 | "matcher": lambda env: env.get("SEMAPHORE"),
41 | "data": lambda env: {
42 | "name": "semaphore",
43 | "branch": self.env.get("BRANCH_NAME"),
44 | "build_identifier": self.env.get("SEMAPHORE_BUILD_NUMBER")
45 | }
46 | }, {
47 | "matcher": lambda env: env.get("JENKINS_URL"),
48 | "data": lambda env: {
49 | "name": "jenkins",
50 | "build_identifier": self.env.get("BUILD_NUMBER"),
51 | "build_url": self.env.get("BUILD_URL"),
52 | "branch": self.env.get("GIT_BRANCH"),
53 | "commit_sha": self.env.get("GIT_COMMIT")
54 | }
55 | }, {
56 | "matcher": lambda env: env.get("TDDIUM"),
57 | "data": lambda env: {
58 | "name": "tddium",
59 | "build_identifier": self.env.get("TDDIUM_SESSION_ID"),
60 | "worker_id": self.env.get("TDDIUM_TID")
61 | }
62 | }, {
63 | "matcher": lambda env: env.get("WERCKER"),
64 | "data": lambda env: {
65 | "name": "wercker",
66 | "build_identifier": self.env.get("WERCKER_BUILD_ID"),
67 | "build_url": self.env.get("WERCKER_BUILD_URL"),
68 | "branch": self.env.get("WERCKER_GIT_BRANCH"),
69 | "commit_sha": self.env.get("WERCKER_GIT_COMMIT")
70 | }
71 | }, {
72 | "matcher": lambda env: env.get("APPVEYOR"),
73 | "data": lambda env: {
74 | "name": "appveyor",
75 | "build_identifier": self.env.get("APPVEYOR_BUILD_ID"),
76 | "build_url": self.env.get("APPVEYOR_API_URL"),
77 | "branch": self.env.get("APPVEYOR_REPO_BRANCH"),
78 | "commit_sha": self.env.get("APPVEYOR_REPO_COMMIT"),
79 | "pull_request": self.env.get("APPVEYOR_PULL_REQUEST_NUMBER")
80 | }
81 | }, {
82 | "matcher": lambda env: self.__ci_name_match("DRONE"),
83 | "data": lambda env: {
84 | "name": "drone",
85 | "build_identifier": self.env.get("CI_BUILD_NUMBER"),
86 | "build_url": self.env.get("CI_BUILD_URL"),
87 | "branch": self.env.get("CI_BRANCH"),
88 | "commit_sha": self.env.get("CI_COMMIT"),
89 | "pull_request": self.env.get("CI_PULL_REQUEST")
90 | }
91 | }, {
92 | "matcher": lambda env: self.__ci_name_match("CODESHIP"),
93 | "data": lambda env: {
94 | "name": "codeship",
95 | "build_identifier": self.env.get("CI_BUILD_NUMBER"),
96 | "build_url": self.env.get("CI_BUILD_URL"),
97 | "branch": self.env.get("CI_BRANCH"),
98 | "commit_sha": self.env.get("CI_COMMIT_ID")
99 | }
100 | }, {
101 | "matcher": lambda env: self.__ci_name_match("VEXOR"),
102 | "data": lambda env: {
103 | "name": "vexor",
104 | "build_identifier": self.env.get("CI_BUILD_NUMBER"),
105 | "build_url": self.env.get("CI_BUILD_URL"),
106 | "branch": self.env.get("CI_BRANCH"),
107 | "commit_sha": self.env.get("CI_BUILD_SHA"),
108 | "pull_request": self.env.get("CI_PULL_REQUEST_ID")
109 | }
110 | }, {
111 | "matcher": lambda env: env.get("BUILDKITE"),
112 | "data": lambda env: {
113 | "name": "buildkite",
114 | "build_identifier": self.env.get("BUILDKITE_JOB_ID"),
115 | "build_url": self.env.get("BUILDKITE_BUILD_URL"),
116 | "branch": self.env.get("BUILDKITE_BRANCH"),
117 | "commit_sha": self.env.get("BUILDKITE_COMMIT"),
118 | }
119 | }, {
120 | "matcher": lambda env: env.get("GITLAB_CI"),
121 | "data": lambda env: {
122 | "name": "gitlab-ci",
123 | "build_identifier": self.env.get("CI_BUILD_ID"),
124 | "branch": self.env.get("CI_BUILD_REF_NAME"),
125 | "commit_sha": self.env.get("CI_BUILD_REF"),
126 | }
127 | }]
128 |
129 | def __ci_name_match(self, pattern):
130 | ci_name = self.env.get("CI_NAME")
131 |
132 | return ci_name and re.match(pattern, ci_name, re.IGNORECASE)
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codeclimate-test-reporter - [DEPRECATED]
2 |
3 | These configuration instructions refer to a language-specific test reporter which is now deprecated in favor of our new unified test reporter client. The [new test reporter](https://docs.codeclimate.com/v1.0/docs/configuring-test-coverage) is faster, distributed as a static binary, has support for parallelized CI builds, and will receive ongoing support by the team here. The existing test reporters for Ruby, Python, PHP, and Javascript are now deprecated.
4 | [![Code Climate][cc-badge]][cc-repo]
5 | [![Test Coverage][cc-coverage-badge]][cc-coverage]
6 | [![PyPI version][pypy-badge]][pypy]
7 |
8 | [cc-badge]: https://codeclimate.com/github/codeclimate/python-test-reporter/badges/gpa.svg
9 | [cc-coverage-badge]: https://codeclimate.com/github/codeclimate/python-test-reporter/badges/coverage.svg
10 | [cc-repo]: https://codeclimate.com/github/codeclimate/python-test-reporter
11 | [cc-coverage]: https://codeclimate.com/github/codeclimate/python-test-reporter/coverage
12 | [pypy-badge]: https://badge.fury.io/py/codeclimate-test-reporter.svg
13 | [pypy]: https://pypi.python.org/pypi/codeclimate-test-reporter
14 |
15 | Collects test coverage data from your Python test suite and sends it to Code
16 | Climate's hosted, automated code review service.
17 |
18 | Code Climate - [https://codeclimate.com][codeclimate.com]
19 |
20 | ## Uploading Your Test Coverage Report
21 |
22 | The `codeclimate-test-reporter` is compatible with [coverage.py][] coverage
23 | reports. By default, coverage.py will generate a `.coverage` file in the current
24 | directory. `codeclimate-test-reporter`, run without arguments, will
25 | look for a coverage report at this default location.
26 |
27 | [coverage.py]: https://coverage.readthedocs.org/
28 |
29 | Note: The `codeclimate-test-reporter` requires a repo token from
30 | [codeclimate.com][], so if you don't have one, the first step is to [signup][]
31 | and configure your repo. Then:
32 |
33 | [codeclimate.com]: https://codeclimate.com
34 | [signup]: https://codeclimate.com/signup
35 |
36 | You can place the repo token in the environment under the key
37 | `CODECLIMATE_REPO_TOKEN` or pass the token as a CLI argument:
38 |
39 | ```console
40 | $ CODECLIMATE_REPO_TOKEN=[token] codeclimate-test-reporter
41 | Submitting payload to https://codeclimate.com... done!
42 | ```
43 |
44 | ```console
45 | $ codeclimate-test-reporter --token [token]
46 | Submitting payload to https://codeclimate.com... done!
47 | ```
48 |
49 | We recommend configuring the repo token in the environment through your CI
50 | settings which will hide the value during runs. The token should be considered a
51 | scoped password. Anyone with the token can submit test coverage data to your
52 | Code Climate repo.
53 |
54 | ```console
55 | # CODECLIMATE_REPO_TOKEN already set in env
56 | $ codeclimate-test-reporter
57 | Submitting payload to https://codeclimate.com... done!
58 | ```
59 |
60 | ### Generating Coverage Reports
61 |
62 | To generate a coverage report with [pytest][], you can use the [pytest-cov][]
63 | plugin:
64 |
65 | ```console
66 | $ py.test --cov=your_package tests/
67 | TOTAL 284 27 90%
68 |
69 | ======================== 14 passed in 0.75 seconds ========================
70 | ```
71 |
72 | To generate a coverage report with [nose][], you can use the [nose cover plugin][]:
73 |
74 | [pytest]: http://pytest.org
75 | [nose]: https://nose.readthedocs.org
76 | [pytest-cov]: https://pypi.python.org/pypi/pytest-cov
77 | [nose cover plugin]: https://nose.readthedocs.org/en/latest/plugins/cover.html
78 |
79 | ```console
80 | $ nosetests --with-coverage --cover-erase --cover-package=your_package
81 | TOTAL 284 27 90%
82 | ----------------------------------------------------------------------
83 | Ran 14 tests in 0.743s
84 |
85 | OK
86 | ```
87 |
88 | By default, coverage.py will create the test coverage report at `./.coverage`.
89 | If you configure coverage.py to generate a coverage report at an alternate
90 | location, pass that to the `codeclimate-test-reporter`:
91 |
92 | ```console
93 | $ codeclimate-test-reporter --file ./alternate/location/.coverage
94 | Submitting payload to https://codeclimate.com... done!
95 | ```
96 |
97 | ## Installation
98 |
99 | You can install the codeclimate-test-reporter using pip:
100 |
101 | ```console
102 | $ pip install codeclimate-test-reporter
103 | Successfully installed codeclimate-test-reporter-[version]
104 | ```
105 |
106 | Or you can add the reporter to your `requirements.txt` file and install it along
107 | with other project dependencies:
108 |
109 | ```txt
110 | # requirements.txt
111 | codeclimate-test-reporter
112 | ```
113 |
114 | ```console
115 | $ pip install -r requirements.txt
116 | Successfully installed codeclimate-test-reporter-[version]
117 | ```
118 |
119 | ## Important FYIs
120 |
121 | Across the many different testing frameworks, setups, and environments, there
122 | are lots of variables at play. Before setting up test coverage, it's important
123 | to understand what we do and do not currently support:
124 |
125 | * **Single payload:** We currently only support a single test coverage payload
126 | per commit. If you run your tests in multiple steps, or via parallel tests,
127 | Code Climate will only process the first payload that we receive. If you are
128 | using a CI, be sure to check if you are running your tests in a parallel mode.
129 |
130 | **Note:** If you've configured Code Climate to analyze multiple languages in
131 | the same repository (e.g., Python and JavaScript), we can nonetheless only
132 | process test coverage information for one of these languages. We'll process
133 | the first payload that we receive.
134 |
135 | * **Invalid File Paths:** By default, our test reporters expect your application
136 | to exist at the root of your repository. If this is not the case, the file
137 | paths in your test coverage payload will not match the file paths that Code
138 | Climate expects.
139 |
140 | ## Troubleshooting
141 |
142 | If you're having trouble setting up or working with our test coverage feature,
143 | see our detailed [help doc][], which covers the most common issues encountered.
144 |
145 | [help doc]: http://docs.codeclimate.com/article/220-help-im-having-trouble-with-test-coverage
146 |
147 | If the issue persists, feel free to [open up an issue][issue] here or contact
148 | help with debug information from the reporter:
149 |
150 | [issue]: https://github.com/codeclimate/python-test-reporter/issues/new
151 |
152 | ```console
153 | $ codeclimate-test-reporter --debug
154 | codeclimate-test-reporter [version]
155 | Requests 2.9.1
156 | Python 3.5.1 (default, Jan 22 2016, 08:54:32)
157 | [GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)]
158 | /usr/local/opt/python3/bin/python3.5
159 | Darwin 15.4.0
160 | ```
161 |
162 | ## Contributions
163 |
164 | Patches, bug fixes, feature requests, and pull requests are welcome.
165 |
166 | ## Development
167 |
168 | A `Dockerfile` is included for developer convenience. Simply run `make test` to
169 | run the test suite and coverage report.
170 |
171 | To release a new version, first run `./bin/prep-release [version]` to bump the
172 | version and open a PR on GitHub.
173 |
174 | Once the PR is merged, run `make release` to publish the new version to pypy and
175 | create a tag on GitHub. This step requires pypy credentials at `~/.pypirc`. You
176 | can copy this repo's `.pypirc.sample` file as a starting point.
177 |
178 | ## Copyright
179 |
180 | See [LICENSE.txt][license]
181 |
182 | [license]: https://github.com/codeclimate/python-test-reporter/blob/master/LICENSE.txt
183 |
--------------------------------------------------------------------------------