diff options
author | Daniel Stender <debian@danielstender.com> | 2015-07-26 22:51:03 +0200 |
---|---|---|
committer | Daniel Stender <debian@danielstender.com> | 2015-07-26 22:51:03 +0200 |
commit | 9f7b3008a9ac46b7780bbc50629e207b4ca365e9 (patch) | |
tree | 1a22055220802442903262e44f9ef0de268543bc |
Imported Upstream version 1.1
-rw-r--r-- | .gitignore | 30 | ||||
-rw-r--r-- | CHANGES.rst | 23 | ||||
-rw-r--r-- | LICENSE.txt | 22 | ||||
-rw-r--r-- | MANIFEST.in | 4 | ||||
-rw-r--r-- | Makefile | 36 | ||||
-rw-r--r-- | README.rst | 127 | ||||
-rw-r--r-- | pytest_catchlog.py | 206 | ||||
-rw-r--r-- | setup.cfg | 5 | ||||
-rwxr-xr-x | setup.py | 46 | ||||
-rw-r--r-- | test_pytest_catchlog.py | 176 | ||||
-rw-r--r-- | tox.ini | 9 |
11 files changed, 684 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15fa13f --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +### Python template +# Byte-compiled / optimized / Native Library Files +__pycache__/ +*.py[cod] +*.so + +# Distribution / packaging +.Python +/env/ +/build/ +/develop-eggs/ +/dist/ +/downloads/ +/eggs/ +/lib/ +/lib64/ +/parts/ +/sdist/ +/var/ +*.egg-info/ +.installed.cfg +*.egg + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..b7d938e --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,23 @@ +Changelog +========= + +List of notable changes between pytest-catchlog releases. + +Version 1.1 +----------- + +Released on 2015-06-07. + +- #2 - Explicitly state Python3 support and add configuration for running + tests with tox on multiple Python versions. (Thanks to Jeremy Bowman!) +- Add an option to silence logs completely on the terminal. + + +Version 1.0 +----------- + +Released on 2014-12-08. + +- Add ``record_tuples`` for comparing recorded log entries against expected + log entries with their logger name, severity and formatted message. + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..b4f4583 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License + +Original work Copyright (c) 2010 Meme Dough +Modified work Copyright (c) 2014 Arthur Skowronek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8ad1455 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include MANIFEST.in Makefile LICENSE.txt README.rst CHANGES.rst setup.cfg + +global-exclude *pyc +prune __pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c664c1b --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +.PHONY: docs +.SILENT: init-devel test test-tox test-coverage + +all: clean test + +clean-coverage: + -rm .coverage* + -rm coverage.xml + -rm -rfv htmlcov + +clean-pyc: + -find . -path './.tox' -prune -or \ + -name '__pycache__' -exec rm -rv {} + + -find . -path './.tox' -prune -or \ + \( -name '*.pyc' -or -name '*.pyo' \) -exec rm -rv {} + + +clean: clean-pyc clean-coverage + -rm -rv build dist *.egg-info + +test: + py.test -v test_pytest_catchlog.py + +test-coverage: + coverage erase + coverage run --source=pytest_catchlog --branch -m pytest -v + coverage report + coverage xml + +audit: + flake8 pytest_catchlog.py + +wheel: + python setup.py bdist_wheel + +sdist: + python setup.py sdist diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..2287412 --- /dev/null +++ b/README.rst @@ -0,0 +1,127 @@ +pytest-catchlog +=============== + +py.test plugin to catch log messages. This is a fork of `pytest-capturelog`_. + +.. _`pytest-capturelog`: https://pypi.python.org/pypi/pytest-capturelog/ + + +Installation +------------ + +The `pytest-catchlog`_ package may be installed with pip or easy_install:: + + pip install pytest-catchlog + easy_install pytest-catchlog + +.. _`pytest-catchlog`: http://pypi.python.org/pypi/pytest-catchlog/ + + +Usage +----- + +If the plugin is installed log messages are captured by default and for +each failed test will be shown in the same manner as captured stdout and +stderr. + +Running without options:: + + py.test test_pytest_catchlog.py + +Shows failed tests like so:: + + -------------------------- Captured log --------------------------- + test_pytest_catchlog.py 26 INFO text going to logger + ------------------------- Captured stdout ------------------------- + text going to stdout + ------------------------- Captured stderr ------------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +By default each captured log message shows the module, line number, +log level and message. Showing the exact module and line number is +useful for testing and debugging. If desired the log format and date +format can be specified to anything that the logging module supports. + +Running pytest specifying formatting options:: + + py.test --log-format="%(asctime)s %(levelname)s %(message)s" \ + --log-date-format="%Y-%m-%d %H:%M:%S" test_pytest_catchlog.py + +Shows failed tests like so:: + + -------------------------- Captured log --------------------------- + 2010-04-10 14:48:44 INFO text going to logger + ------------------------- Captured stdout ------------------------- + text going to stdout + ------------------------- Captured stderr ------------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +Further it is possible to disable reporting logs on failed tests +completely with:: + + py.test --no-print-logs test_pytest_catchlog.py + +Shows failed tests in the normal manner as no logs were captured:: + + ------------------------- Captured stdout ------------------------- + text going to stdout + ------------------------- Captured stderr ------------------------- + text going to stderr + ==================== 2 failed in 0.02 seconds ===================== + +Inside tests it is possible to change the log level for the captured +log messages. This is supported by the ``caplog`` funcarg:: + + def test_foo(caplog): + caplog.setLevel(logging.INFO) + pass + +By default the level is set on the handler used to catch the log +messages, however as a convenience it is also possible to set the log +level of any logger:: + + def test_foo(caplog): + caplog.setLevel(logging.CRITICAL, logger='root.baz') + pass + +It is also possible to use a context manager to temporarily change the +log level:: + + def test_bar(caplog): + with caplog.atLevel(logging.INFO): + pass + +Again, by default the level of the handler is affected but the level +of any logger can be changed instead with:: + + def test_bar(caplog): + with caplog.atLevel(logging.CRITICAL, logger='root.baz'): + pass + +Lastly all the logs sent to the logger during the test run are made +available on the funcarg in the form of both the LogRecord instances +and the final log text. This is useful for when you want to assert on +the contents of a message:: + + def test_baz(caplog): + func_under_test() + for record in caplog.records(): + assert record.levelname != 'CRITICAL' + assert 'wally' not in caplog.text() + +For all the available attributes of the log records see the +``logging.LogRecord`` class. + +You can also resort to ``record_tuples`` if all you want to do is to ensure, +that certain messages have been logged under a given logger name with a +given severity and message:: + + def test_foo(caplog): + logging.getLogger().info('boo %s', 'arg') + + assert caplog.record_tuples() == [ + ('root', logging.INFO, 'boo arg'), + ] + diff --git a/pytest_catchlog.py b/pytest_catchlog.py new file mode 100644 index 0000000..61cdb94 --- /dev/null +++ b/pytest_catchlog.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +from __future__ import (absolute_import, print_function, + unicode_literals, division) + +import logging + +import py + + +__version__ = '1.1' + + +def pytest_addoption(parser): + """Add options to control log capturing.""" + + group = parser.getgroup('catchlog', 'Log catching.') + group.addoption('--no-print-logs', + dest='log_print', action='store_false', default=True, + help='disable printing caught logs on failed tests.') + group.addoption('--log-format', + dest='log_format', + default=('%(filename)-25s %(lineno)4d' + ' %(levelname)-8s %(message)s'), + help='log format as used by the logging module.') + group.addoption('--log-date-format', + dest='log_date_format', default=None, + help='log date format as used by the logging module.') + + +def pytest_configure(config): + """Always register the log catcher plugin with py.test or tests can't + find the fixture function. + """ + config.pluginmanager.register(CatchLogPlugin(config), '_catch_log') + + +class CatchLogPlugin(object): + """Attaches to the logging module and captures log messages for each test. + """ + + def __init__(self, config): + """Creates a new plugin to capture log messages. + + The formatter can be safely shared across all handlers so + create a single one for the entire test session here. + """ + self.print_logs = config.getvalue('log_print') + self.formatter = logging.Formatter(config.getvalue('log_format'), + config.getvalue('log_date_format')) + + def pytest_runtest_setup(self, item): + """Start capturing log messages for this test. + + Creating a specific handler for each test ensures that we + avoid multi threading issues. + + Attaching the handler and setting the level at the beginning + of each test ensures that we are setup to capture log + messages. + """ + + # Create a handler for this test. + item.catch_log_handler = CatchLogHandler() + item.catch_log_handler.setFormatter(self.formatter) + + # Attach the handler to the root logger and ensure that the + # root logger is set to log all levels. + root_logger = logging.getLogger() + root_logger.addHandler(item.catch_log_handler) + root_logger.setLevel(logging.NOTSET) + + def pytest_runtest_makereport(self, __multicall__, item, call): + """Add captured log messages for this report.""" + + report = __multicall__.execute() + + # This fn called after setup, call and teardown. Only + # interested in just after test call has finished. + if call.when == 'call': + + # Detach the handler from the root logger to ensure no + # further access to the handler. + root_logger = logging.getLogger() + root_logger.removeHandler(item.catch_log_handler) + + # For failed tests that have captured log messages add a + # captured log section to the report if desired. + if not report.passed and self.print_logs: + long_repr = getattr(report, 'longrepr', None) + if hasattr(long_repr, 'addsection'): + log = item.catch_log_handler.stream.getvalue().strip() + if log: + long_repr.addsection('Captured log', log) + + # Release the handler resources. + item.catch_log_handler.close() + del item.catch_log_handler + + return report + + +class CatchLogHandler(logging.StreamHandler): + """A logging handler that stores log records and the log text.""" + + def __init__(self): + """Creates a new log handler.""" + + logging.StreamHandler.__init__(self) + self.stream = py.io.TextIO() + self.records = [] + + def close(self): + """Close this log handler and its underlying stream.""" + + logging.StreamHandler.close(self) + self.stream.close() + + def emit(self, record): + """Keep the log records in a list in addition to the log text.""" + + self.records.append(record) + logging.StreamHandler.emit(self, record) + + +class CatchLogFuncArg(object): + """Provides access and control of log capturing.""" + + def __init__(self, handler): + """Creates a new funcarg.""" + + self.handler = handler + + def text(self): + """Returns the log text.""" + + return self.handler.stream.getvalue() + + def records(self): + """Returns the list of log records.""" + + return self.handler.records + + def record_tuples(self): + """Returns a list of a striped down version of log records intended + for use in assertion comparison. + + The format of the tuple is: + + (logger_name, log_level, message) + """ + return [(r.name, r.levelno, r.getMessage()) for r in self.records()] + + def set_level(self, level, logger=None): + """Sets the level for capturing of logs. + + By default, the level is set on the handler used to capture + logs. Specify a logger name to instead set the level of any + logger. + """ + + obj = logger and logging.getLogger(logger) or self.handler + obj.setLevel(level) + + def at_level(self, level, logger=None): + """Context manager that sets the level for capturing of logs. + + By default, the level is set on the handler used to capture + logs. Specify a logger name to instead set the level of any + logger. + """ + + obj = logger and logging.getLogger(logger) or self.handler + return CatchLogLevel(obj, level) + + +class CatchLogLevel(object): + """Context manager that sets the logging level of a handler or logger.""" + + def __init__(self, obj, level): + """Creates a new log level context manager.""" + + self.obj = obj + self.level = level + + def __enter__(self): + """Adjust the log level.""" + + self.orig_level = self.obj.level + self.obj.setLevel(self.level) + + def __exit__(self, exc_type, exc_value, traceback): + """Restore the log level.""" + + self.obj.setLevel(self.orig_level) + + +def pytest_funcarg__caplog(request): + """Returns a funcarg to access and control log capturing.""" + + return CatchLogFuncArg(request._pyfuncitem.catch_log_handler) + + +def pytest_funcarg__capturelog(request): + """Returns a funcarg to access and control log capturing.""" + + return CatchLogFuncArg(request._pyfuncitem.catch_log_handler) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ca1cfaf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[wheel] +universal = 1 + +[sdist] +formats = zip diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..ba84092 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +import io +import os +import re + +from setuptools import setup + + +def _read_text_file(file_name): + file_path = os.path.join(os.path.dirname(__file__), file_name) + with io.open(file_path, encoding='utf-8') as f_stream: + return f_stream.read() + + +def _get_version(): + return re.search("__version__\s*=\s*'([^']+)'\s*", + _read_text_file('pytest_catchlog.py')).group(1) + + +setup(name='pytest-catchlog', + version=_get_version(), + description=('py.test plugin to catch log messages.' + ' This is a fork of pytest-capturelog.'), + long_description=_read_text_file('README.rst'), + author='Arthur Skowronek (Fork Author)', # original author: Meme Dough + author_email='eisensheng@mailbox.org', + url='https://github.com/eisensheng/pytest-catchlog', + py_modules=['pytest_catchlog', ], + install_requires=['py>=1.1.1', ], + entry_points={'pytest11': ['pytest_catchlog = pytest_catchlog']}, + license='MIT License', + zip_safe=False, + keywords='py.test pytest', + classifiers=['Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Testing']) diff --git a/test_pytest_catchlog.py b/test_pytest_catchlog.py new file mode 100644 index 0000000..0e9d55f --- /dev/null +++ b/test_pytest_catchlog.py @@ -0,0 +1,176 @@ +import py + +pytest_plugins = 'pytester', 'catchlog' + + +def test_nothing_logged(testdir): + testdir.makepyfile(''' + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(): + sys.stdout.write('text going to stdout') + sys.stderr.write('text going to stderr') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured log -*']) + + +def test_messages_logged(testdir): + testdir.makepyfile(''' + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(): + sys.stdout.write('text going to stdout') + sys.stderr.write('text going to stderr') + logging.getLogger().info('text going to logger') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured log -*', + '*text going to logger*']) + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + + +def test_change_level(testdir): + testdir.makepyfile(''' + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(caplog): + caplog.set_level(logging.INFO) + log = logging.getLogger() + log.debug('handler DEBUG level') + log.info('handler INFO level') + + caplog.set_level(logging.CRITICAL, logger='root.baz') + log = logging.getLogger('root.baz') + log.warning('logger WARNING level') + log.critical('logger CRITICAL level') + + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured log -*', + '*handler INFO level*', + '*logger CRITICAL level*']) + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured log -*', '*handler DEBUG level*']) + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured log -*', '*logger WARNING level*']) + + +@py.test.mark.skipif('sys.version_info < (2,5)') +def test_with_statement(testdir): + testdir.makepyfile(''' + from __future__ import with_statement + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(caplog): + with caplog.at_level(logging.INFO): + log = logging.getLogger() + log.debug('handler DEBUG level') + log.info('handler INFO level') + + with caplog.at_level(logging.CRITICAL, logger='root.baz'): + log = logging.getLogger('root.baz') + log.warning('logger WARNING level') + log.critical('logger CRITICAL level') + + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured log -*', + '*handler INFO level*', + '*logger CRITICAL level*']) + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured log -*', '*handler DEBUG level*']) + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured log -*', '*logger WARNING level*']) + + +def test_log_access(testdir): + testdir.makepyfile(''' + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(caplog): + logging.getLogger().info('boo %s', 'arg') + assert caplog.records()[0].levelname == 'INFO' + assert caplog.records()[0].msg == 'boo %s' + assert 'boo arg' in caplog.text() + ''') + result = testdir.runpytest() + assert result.ret == 0 + + +def test_funcarg_help(testdir): + result = testdir.runpytest('--funcargs') + result.stdout.fnmatch_lines(['*caplog*']) + + +def test_record_tuples(testdir): + testdir.makepyfile(''' + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(caplog): + logging.getLogger().info('boo %s', 'arg') + + assert caplog.record_tuples() == [ + ('root', logging.INFO, 'boo arg'), + ] + ''') + result = testdir.runpytest() + assert result.ret == 0 + + +def test_disable_log_capturing(testdir): + testdir.makepyfile(''' + import sys + import logging + + pytest_plugins = 'catchlog' + + def test_foo(caplog): + sys.stdout.write('text going to stdout') + logging.getLogger().warning('catch me if you can!') + sys.stderr.write('text going to stderr') + assert False + ''') + result = testdir.runpytest('--no-print-logs') + print(result.stdout) + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured stdout call -*', + 'text going to stdout']) + result.stdout.fnmatch_lines(['*- Captured stderr call -*', + 'text going to stderr']) + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured log -*']) @@ -0,0 +1,9 @@ +[tox] +envlist = py{27,32,33,34,py,py3} + +[testenv] +deps = + py==1.4.28 + pytest==2.7.1 +commands = + py.test {posargs:test_pytest_catchlog.py} |