132nd-etcher/epab

View on GitHub
epab/cmd/_pytest.py

Summary

Maintainability
A
2 hrs
Test Coverage
# coding=utf-8
"""
Manages the test suite
"""
import logging
import os
import shutil
import webbrowser
from pathlib import Path

import click
import elib_run

import epab.utils
from epab.core import CTX, config

LOGGER = logging.getLogger('EPAB')
PYTEST_OPTIONS = ' '.join([
    '--cov={package}',
    '--cov-report xml',
    '--cov-report html',
    '--cov-branch',
    # f'--cov-fail-under={CONFIG.test__coverage__fail_under}',
    '--durations={test_duration}',
    # '--hypothesis-show-statistics',
    '--tb=short',
    '--cov-config .coveragerc',
    # '--dead-fixtures',
    # '--dup-fixtures',
    # '-x',
])

# noinspection SpellCheckingInspection
COVERAGE_CONFIG = r"""
## http://coverage.readthedocs.io/en/latest/config.html
[run]
# timid = True
branch = True
source = epab
# omit =

[report]
# Regexes for lines to exclude from consideration
exclude_lines =
    # Have to re-enable the standard pragma
    pragma: no cover

    # Don't complain about missing debug-only code:
    def __repr__
    if self\.debug

    # Don't complain if tests don't hit defensive assertion code:
    raise AssertionError
    raise NotImplementedError
    pass

    # Ignore abstract definitions:
    @abc.abstractmethod
    @abc.abstractproperty

    # Ignore click commands
    # @click.command()

    # Don't complain if non-runnable code isn't run:
    if 0:
    if __name__ == .__main__.:

[html]
directory = ./htmlcov
title = Coverage report
"""


class _Coverage:
    @staticmethod
    def install():
        """
        Installs coverage config file
        """
        Path('.coveragerc').write_text(COVERAGE_CONFIG.format(package_name=config.PACKAGE_NAME()))

    @staticmethod
    def upload_coverage_to_codacy():
        """
        Uploads the coverage to Codacy
        """
        if not Path('coverage.xml').exists():
            LOGGER.error('coverage.xml not found, skipping codacy coverage')
            return
        if os.getenv('CODACY_PROJECT_TOKEN') is None:
            LOGGER.error('CODACY_PROJECT_TOKEN env var not defined, skipping codacy coverage')
            return

        LOGGER.info('uploading coverage to Codacy')
        elib_run.run('pip install --upgrade codacy-coverage')
        elib_run.run('python-codacy-coverage -r coverage.xml')
        LOGGER.info('codacy coverage OK')

    # Disabled for the time being
    # @staticmethod
    # def upload_coverage_to_scrutinizer():
    #     """
    #     Uploads the coverage to Scrutinizer
    #     """
    #     if os.getenv('SCRUT_TOK', False):
    #         if Path('coverage.xml').exists():
    #             LOGGER.info('Uploading coverage to Scrutinizer')
    #             elib_run.run('pip install git+https://github.com/etcher-vault/ocular.py.git#egg=ocular')
    #             token = os.getenv('SCRUT_TOK')
    #             elib_run.run(
    #                 f'ocular --access-token "{token}" --data-file "coverage.xml" --config-file ".coveragerc"'
    #             )
    #             LOGGER.info('Scrutinizer coverage OK')
    #         else:
    #             LOGGER.error('"coverage.xml" not found, skipping ocular coverage')
    #     else:
    #         LOGGER.error('no "SCRUT_TOK" in environment, skipping ocular coverage')

    @staticmethod
    def remove_config_file():
        """
        Removes coverage config file
        """
        try:
            Path('.coveragerc').unlink()
        except FileNotFoundError:
            pass


def upload_coverage():
    """
    Sends coverage result to Codacy and Scrutinizer if running on AV
    """
    if CTX.appveyor:
        # _Coverage.upload_coverage_to_scrutinizer()
        _Coverage.upload_coverage_to_codacy()
    else:
        LOGGER.info('skipping coverage upload')


def pytest_options():
    """
    Returns: PyTest standard command line options
    """
    return PYTEST_OPTIONS.format(
        package=config.PACKAGE_NAME(),
        test_duration=config.TEST_DURATION_COUNT(),
    )


@epab.utils.run_once
@epab.utils.timeit
def _pytest(test, *, long, show, exitfirst, last_failed, failed_first, rm_cov):
    LOGGER.info('running test suite')
    os.environ['PYTEST_QT_API'] = 'pyqt5'
    _Coverage.install()
    cmd = f'pytest {test}'

    if CTX.appveyor:
        LOGGER.debug('running on AV; VCR recording disabled')
        cmd = f'{cmd} --vcr-record=none'

    if CTX.appveyor and config.TEST_AV_RUNNER_OPTIONS():
        cmd = f'{cmd} {config.TEST_AV_RUNNER_OPTIONS()}'
    elif config.TEST_RUNNER_OPTIONS():
        cmd = f'{cmd} {config.TEST_RUNNER_OPTIONS()}'

    long = ' --long' if long else ''
    exitfirst = ' --exitfirst' if exitfirst else ''
    last_failed = ' --last-failed' if last_failed else ''
    failed_first = ' --failed-first' if failed_first else ''

    if rm_cov and Path('./htmlcov').exists():
        shutil.rmtree('./htmlcov')
    cmd = f'{cmd} {pytest_options()}{long}{exitfirst}{last_failed}{failed_first}'

    try:
        elib_run.run(cmd, timeout=config.TEST_PYTEST_TIMEOUT())
    finally:
        upload_coverage()
        _Coverage.remove_config_file()
    if show:
        # noinspection SpellCheckingInspection
        path = Path('./htmlcov/index.html').absolute()
        webbrowser.open(f'file://{path}')


@click.command(context_settings=dict(ignore_unknown_options=True))
@click.option('-l', '--long', is_flag=True, default=False, help='Long tests')
@click.option('-s', '--show', is_flag=True, default=False, help='Show coverage in browser')
@click.option('-x', '--exitfirst', is_flag=True, default=False, help='Exit instantly on first error')
@click.option('-r', '--rm-cov', is_flag=True, default=False, help='Delete coverage report from previous runs')
@click.option('-lf', '--last-failed', is_flag=True, default=False, help='Rerun only the tests that failed')
@click.option('-ff', '--failed-first', is_flag=True, default=False,
              help='Run all tests but run the last failures first')
@click.option('-t', '--test', default=config.TEST_TARGET(), help='Select which tests to run')
def pytest(test, long, show, exitfirst, last_failed, failed_first, rm_cov):
    """
    Runs Pytest (https://docs.pytest.org/en/latest/)
    """
    _pytest(
        test,
        long=long,
        show=show,
        exitfirst=exitfirst,
        last_failed=last_failed,
        failed_first=failed_first,
        rm_cov=rm_cov,
    )