From 24a51df9cb49ec57256252e1f5b0528d41d6cc54 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Wed, 15 Feb 2017 00:27:14 +0100 Subject: [PATCH] extract full-coverage pytest plugin --- setup.cfg | 31 +++++++++ test/conftest.py | 124 +++-------------------------------- test/full_coverage_plugin.py | 119 +++++++++++++++++++++++++++++++++ tox.ini | 11 +--- 4 files changed, 163 insertions(+), 122 deletions(-) create mode 100644 test/full_coverage_plugin.py diff --git a/setup.cfg b/setup.cfg index 39148f988..d01e12f34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,3 +18,34 @@ show_missing = True exclude_lines = pragma: no cover raise NotImplementedError() + +[tool:full_coverage] +exclude = + mitmproxy/contentviews/__init__.py + mitmproxy/contentviews/protobuf.py + mitmproxy/contentviews/wbxml.py + mitmproxy/contentviews/xml_html.py + mitmproxy/net/tcp.py + mitmproxy/net/http/cookies.py + mitmproxy/net/http/encoding.py + mitmproxy/net/http/message.py + mitmproxy/net/http/url.py + mitmproxy/proxy/protocol/ + mitmproxy/proxy/config.py + mitmproxy/proxy/root_context.py + mitmproxy/proxy/server.py + mitmproxy/tools/ + mitmproxy/certs.py + mitmproxy/connections.py + mitmproxy/controller.py + mitmproxy/export.py + mitmproxy/flow.py + mitmproxy/flowfilter.py + mitmproxy/http.py + mitmproxy/io_compat.py + mitmproxy/master.py + mitmproxy/optmanager.py + pathod/pathoc.py + pathod/pathod.py + pathod/test.py + pathod/protocols/http2.py diff --git a/test/conftest.py b/test/conftest.py index 83823a19d..b4e1da932 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -6,6 +6,7 @@ from contextlib import contextmanager import mitmproxy.net.tcp +pytest_plugins = ('test.full_coverage_plugin',) requires_alpn = pytest.mark.skipif( not mitmproxy.net.tcp.HAS_ALPN, @@ -27,10 +28,17 @@ skip_appveyor = pytest.mark.skipif( ) +@pytest.fixture() +def disable_alpn(monkeypatch): + monkeypatch.setattr(mitmproxy.net.tcp, 'HAS_ALPN', False) + monkeypatch.setattr(OpenSSL.SSL._lib, 'Cryptography_HAS_ALPN', False) + + +################################################################################ +# TODO: remove this wrapper when pytest 3.1.0 is released original_pytest_raises = pytest.raises -# TODO: remove this wrapper when pytest 3.1.0 is released @contextmanager @functools.wraps(original_pytest_raises) def raises(exc, *args, **kwargs): @@ -41,116 +49,4 @@ def raises(exc, *args, **kwargs): pytest.raises = raises - - -@pytest.fixture() -def disable_alpn(monkeypatch): - monkeypatch.setattr(mitmproxy.net.tcp, 'HAS_ALPN', False) - monkeypatch.setattr(OpenSSL.SSL._lib, 'Cryptography_HAS_ALPN', False) - - -enable_coverage = False -coverage_values = [] -coverage_passed = False - - -def pytest_addoption(parser): - parser.addoption('--full-cov', - action='append', - dest='full_cov', - default=[], - help="Require full test coverage of 100%% for this module/path/filename (multi-allowed). Default: none") - - parser.addoption('--no-full-cov', - action='append', - dest='no_full_cov', - default=[], - help="Exclude file from a parent 100%% coverage requirement (multi-allowed). Default: none") - - -def pytest_configure(config): - global enable_coverage - enable_coverage = ( - len(config.getoption('file_or_dir')) == 0 and - len(config.getoption('full_cov')) > 0 and - config.pluginmanager.getplugin("_cov") is not None and - config.pluginmanager.getplugin("_cov").cov_controller is not None and - config.pluginmanager.getplugin("_cov").cov_controller.cov is not None - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_runtestloop(session): - global enable_coverage - global coverage_values - global coverage_passed - - if not enable_coverage: - yield - return - - cov = pytest.config.pluginmanager.getplugin("_cov").cov_controller.cov - - if os.name == 'nt': - cov.exclude('pragma: windows no cover') - - yield - - coverage_values = dict([(name, 0) for name in pytest.config.option.full_cov]) - - prefix = os.getcwd() - excluded_files = [os.path.normpath(f) for f in pytest.config.option.no_full_cov] - measured_files = [os.path.normpath(os.path.relpath(f, prefix)) for f in cov.get_data().measured_files()] - measured_files = [f for f in measured_files if not any(f.startswith(excluded_f) for excluded_f in excluded_files)] - - for name in coverage_values.keys(): - files = [f for f in measured_files if f.startswith(os.path.normpath(name))] - try: - with open(os.devnull, 'w') as null: - overall = cov.report(files, ignore_errors=True, file=null) - singles = [(s, cov.report(s, ignore_errors=True, file=null)) for s in files] - coverage_values[name] = (overall, singles) - except: - pass - - if any(v < 100 for v, _ in coverage_values.values()): - # make sure we get the EXIT_TESTSFAILED exit code - session.testsfailed += 1 - else: - coverage_passed = True - - -def pytest_terminal_summary(terminalreporter, exitstatus): - global enable_coverage - global coverage_values - global coverage_passed - - if not enable_coverage: - return - - terminalreporter.write('\n') - if not coverage_passed: - markup = {'red': True, 'bold': True} - msg = "FAIL: Full test coverage not reached!\n" - terminalreporter.write(msg, **markup) - - for name in sorted(coverage_values.keys()): - msg = 'Coverage for {}: {:.2f}%\n'.format(name, coverage_values[name][0]) - if coverage_values[name][0] < 100: - markup = {'red': True, 'bold': True} - for s, v in sorted(coverage_values[name][1]): - if v < 100: - msg += ' {}: {:.2f}%\n'.format(s, v) - else: - markup = {'green': True} - terminalreporter.write(msg, **markup) - else: - markup = {'green': True} - msg = 'SUCCESS: Full test coverage reached in modules and files:\n' - msg += '{}\n\n'.format('\n'.join(pytest.config.option.full_cov)) - terminalreporter.write(msg, **markup) - - msg = '\nExcluded files:\n' - for s in sorted(pytest.config.option.no_full_cov): - msg += " {}\n".format(s) - terminalreporter.write(msg) +################################################################################ diff --git a/test/full_coverage_plugin.py b/test/full_coverage_plugin.py new file mode 100644 index 000000000..e9951af9f --- /dev/null +++ b/test/full_coverage_plugin.py @@ -0,0 +1,119 @@ +import os +import configparser +import pytest + + +enable_coverage = False +coverage_values = [] +coverage_passed = True +no_full_cov = [] + + +def pytest_addoption(parser): + parser.addoption('--full-cov', + action='append', + dest='full_cov', + default=[], + help="Require full test coverage of 100%% for this module/path/filename (multi-allowed). Default: none") + + parser.addoption('--no-full-cov', + action='append', + dest='no_full_cov', + default=[], + help="Exclude file from a parent 100%% coverage requirement (multi-allowed). Default: none") + + +def pytest_configure(config): + global enable_coverage + global no_full_cov + + enable_coverage = ( + len(config.getoption('file_or_dir')) == 0 and + len(config.getoption('full_cov')) > 0 and + config.pluginmanager.getplugin("_cov") is not None and + config.pluginmanager.getplugin("_cov").cov_controller is not None and + config.pluginmanager.getplugin("_cov").cov_controller.cov is not None + ) + + c = configparser.ConfigParser() + c.read('setup.cfg') + fs = c['tool:full_coverage']['exclude'].split('\n') + no_full_cov = config.option.no_full_cov + [f.strip() for f in fs] + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtestloop(session): + global enable_coverage + global coverage_values + global coverage_passed + global no_full_cov + + if not enable_coverage: + yield + return + + cov = pytest.config.pluginmanager.getplugin("_cov").cov_controller.cov + + if os.name == 'nt': + cov.exclude('pragma: windows no cover') + + yield + + coverage_values = dict([(name, 0) for name in pytest.config.option.full_cov]) + + prefix = os.getcwd() + + excluded_files = [os.path.normpath(f) for f in no_full_cov] + measured_files = [os.path.normpath(os.path.relpath(f, prefix)) for f in cov.get_data().measured_files()] + measured_files = [f for f in measured_files if not any(f.startswith(excluded_f) for excluded_f in excluded_files)] + + for name in coverage_values.keys(): + files = [f for f in measured_files if f.startswith(os.path.normpath(name))] + try: + with open(os.devnull, 'w') as null: + overall = cov.report(files, ignore_errors=True, file=null) + singles = [(s, cov.report(s, ignore_errors=True, file=null)) for s in files] + coverage_values[name] = (overall, singles) + except: + pass + + if any(v < 100 for v, _ in coverage_values.values()): + # make sure we get the EXIT_TESTSFAILED exit code + session.testsfailed += 1 + coverage_passed = False + + +def pytest_terminal_summary(terminalreporter, exitstatus): + global enable_coverage + global coverage_values + global coverage_passed + global no_full_cov + + if not enable_coverage: + return + + terminalreporter.write('\n') + if not coverage_passed: + markup = {'red': True, 'bold': True} + msg = "FAIL: Full test coverage not reached!\n" + terminalreporter.write(msg, **markup) + + for name in sorted(coverage_values.keys()): + msg = 'Coverage for {}: {:.2f}%\n'.format(name, coverage_values[name][0]) + if coverage_values[name][0] < 100: + markup = {'red': True, 'bold': True} + for s, v in sorted(coverage_values[name][1]): + if v < 100: + msg += ' {}: {:.2f}%\n'.format(s, v) + else: + markup = {'green': True} + terminalreporter.write(msg, **markup) + else: + msg = 'SUCCESS: Full test coverage reached in modules and files:\n' + msg += '{}\n\n'.format('\n'.join(pytest.config.option.full_cov)) + terminalreporter.write(msg, green=True) + + msg = '\nExcluded files:\n' + for s in sorted(no_full_cov): + msg += " {}\n".format(s) + terminalreporter.write(msg) diff --git a/tox.ini b/tox.ini index 1c5e1b963..352d0e3c1 100644 --- a/tox.ini +++ b/tox.ini @@ -11,14 +11,9 @@ passenv = CODECOV_TOKEN CI CI_* TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* SNAPSHOT_* O setenv = HOME = {envtmpdir} commands = mitmdump --version - pytest --timeout 60 --cov-report='' --cov=mitmproxy --cov=pathod \ - --full-cov=mitmproxy/ \ - --no-full-cov=mitmproxy/contentviews/__init__.py --no-full-cov=mitmproxy/contentviews/protobuf.py --no-full-cov=mitmproxy/contentviews/wbxml.py --no-full-cov=mitmproxy/contentviews/xml_html.py \ - --no-full-cov=mitmproxy/net/tcp.py --no-full-cov=mitmproxy/net/http/cookies.py --no-full-cov=mitmproxy/net/http/encoding.py --no-full-cov=mitmproxy/net/http/message.py --no-full-cov=mitmproxy/net/http/url.py \ - --no-full-cov=mitmproxy/proxy/protocol/ --no-full-cov=mitmproxy/proxy/config.py --no-full-cov=mitmproxy/proxy/root_context.py --no-full-cov=mitmproxy/proxy/server.py \ - --no-full-cov=mitmproxy/tools/ \ - --no-full-cov=mitmproxy/certs.py --no-full-cov=mitmproxy/connections.py --no-full-cov=mitmproxy/controller.py --no-full-cov=mitmproxy/export.py --no-full-cov=mitmproxy/flow.py --no-full-cov=mitmproxy/flowfilter.py --no-full-cov=mitmproxy/http.py --no-full-cov=mitmproxy/io_compat.py --no-full-cov=mitmproxy/master.py --no-full-cov=mitmproxy/optmanager.py \ - --full-cov=pathod/ --no-full-cov=pathod/pathoc.py --no-full-cov=pathod/pathod.py --no-full-cov=pathod/test.py --no-full-cov=pathod/protocols/http2.py \ + pytest --timeout 60 --cov-report='' \ + --cov=mitmproxy --cov=pathod \ + --full-cov=mitmproxy/ --full-cov=pathod/ \ {posargs} {env:CI_COMMANDS:python -c ""}