summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Stender <debian@danielstender.com>2015-07-26 22:51:03 +0200
committerDaniel Stender <debian@danielstender.com>2015-07-26 22:51:03 +0200
commit9f7b3008a9ac46b7780bbc50629e207b4ca365e9 (patch)
tree1a22055220802442903262e44f9ef0de268543bc
Imported Upstream version 1.1
-rw-r--r--.gitignore30
-rw-r--r--CHANGES.rst23
-rw-r--r--LICENSE.txt22
-rw-r--r--MANIFEST.in4
-rw-r--r--Makefile36
-rw-r--r--README.rst127
-rw-r--r--pytest_catchlog.py206
-rw-r--r--setup.cfg5
-rwxr-xr-xsetup.py46
-rw-r--r--test_pytest_catchlog.py176
-rw-r--r--tox.ini9
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 -*'])
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..3be9fcb
--- /dev/null
+++ b/tox.ini
@@ -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}