From 404dfc09250a5bb3e820347ff1ecdfb9e9f7fb85 Mon Sep 17 00:00:00 2001 From: Daniel Stender Date: Wed, 30 Dec 2015 19:25:18 +0100 Subject: Import pytest-catchlog_1.2.1.orig.tar.xz --- .travis.yml | 20 ++++ CHANGES.rst | 48 +++++++- MANIFEST.in | 1 + README.rst | 42 ++++--- pytest_catchlog.py | 287 +++++++++++++++++++++++++++++++++--------------- setup.py | 9 +- tasks.py | 188 +++++++++++++++++++++++++++++++ test_pytest_catchlog.py | 156 +++++++++++++++++++++----- tox.ini | 2 +- 9 files changed, 615 insertions(+), 138 deletions(-) create mode 100644 .travis.yml create mode 100644 tasks.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..db995e2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +sudo: false +language: python +python: + - "2.6" + - "2.7" + - "3.2" + - "3.3" + - "3.4" + - "3.5" + - "pypy" + - "pypy3" + +install: + - pip install -e . +script: + - py.test + +cache: + directories: + - $HOME/.cache/pip/http diff --git a/CHANGES.rst b/CHANGES.rst index b7d938e..ed164ab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,53 @@ Changelog List of notable changes between pytest-catchlog releases. +.. %UNRELEASED_SECTION% + +`Unreleased`_ +------------- + +Yet to be released. + + +`1.2.1`_ +------------- + +Released on 2015-12-07. + +- [Bugfix] #18 - Allow ``caplog.records()`` to be modified. Thanks to Eldar Abusalimov for the PR and Marco Nenciarini for reporting the issue. +- [Bugfix] #15 #17 - Restore Python 2.6 compatibility. (Thanks to Marco Nenciarini!) + +.. attention:: + Deprecation warning: the following objects (i.e. functions, properties) + are slated for removal in the next major release. + + - ``caplog.at_level`` and ``caplog.set_level`` should be used instead of + ``caplog.atLevel`` and ``caplog.setLevel``. + + The methods ``caplog.atLevel`` and ``caplog.setLevel`` are still + available but deprecated and not supported since they don't follow + the PEP8 convention for method names. + + - ``caplog.text``, ``caplog.records`` and + ``caplog.record_tuples`` were turned into properties. + They still can be used as regular methods for backward compatibility, + but that syntax is considered deprecated and scheduled for removal in + the next major release. + + +Version 1.2 +----------- + +Released on 2015-11-08. + +- [Feature] #6 - Configure logging message and date format through ini file. +- [Feature] #7 - Also catch logs from setup and teardown stages. +- [Feature] #7 - Replace deprecated ``__multicall__`` use to support future Py.test releases. +- [Feature] #11 - reintroduce ``setLevel`` and ``atLevel`` to retain backward compatibility with pytest-capturelog. Also the members ``text``, ``records`` and ``record_tuples`` of the ``caplog`` fixture can be used as properties now. + +Special thanks for this release goes to Eldar Abusalimov. He provided all of the changed features. + + Version 1.1 ----------- @@ -20,4 +67,3 @@ 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/MANIFEST.in b/MANIFEST.in index 8ad1455..bc85cc3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include MANIFEST.in Makefile LICENSE.txt README.rst CHANGES.rst setup.cfg +include test_pytest_catchlog.py global-exclude *pyc prune __pycache__ diff --git a/README.rst b/README.rst index 2287412..bc7fcba 100644 --- a/README.rst +++ b/README.rst @@ -30,11 +30,11 @@ Running without options:: Shows failed tests like so:: - -------------------------- Captured log --------------------------- + ----------------------- Captured stdlog call ---------------------- test_pytest_catchlog.py 26 INFO text going to logger - ------------------------- Captured stdout ------------------------- + ----------------------- Captured stdout call ---------------------- text going to stdout - ------------------------- Captured stderr ------------------------- + ----------------------- Captured stderr call ---------------------- text going to stderr ==================== 2 failed in 0.02 seconds ===================== @@ -50,14 +50,26 @@ Running pytest specifying formatting options:: Shows failed tests like so:: - -------------------------- Captured log --------------------------- + ----------------------- Captured stdlog call ---------------------- 2010-04-10 14:48:44 INFO text going to logger - ------------------------- Captured stdout ------------------------- + ----------------------- Captured stdout call ---------------------- text going to stdout - ------------------------- Captured stderr ------------------------- + ----------------------- Captured stderr call ---------------------- text going to stderr ==================== 2 failed in 0.02 seconds ===================== +These options can also be customized through a configuration file:: + + [pytest] + log_format = %(asctime)s %(levelname)s %(message)s + log_date_format = %Y-%m-%d %H:%M:%S + +Although the same effect could be achieved through the ``addopts`` setting, +using dedicated options should be preferred since the latter doesn't +force other developers to have ``pytest-catchlog`` installed (while at +the same time, ``addopts`` approach would fail with 'unrecognized arguments' +error). Command line arguments take precedence. + Further it is possible to disable reporting logs on failed tests completely with:: @@ -65,9 +77,9 @@ completely with:: Shows failed tests in the normal manner as no logs were captured:: - ------------------------- Captured stdout ------------------------- + ----------------------- Captured stdout call ---------------------- text going to stdout - ------------------------- Captured stderr ------------------------- + ----------------------- Captured stderr call ---------------------- text going to stderr ==================== 2 failed in 0.02 seconds ===================== @@ -75,7 +87,7 @@ 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) + caplog.set_level(logging.INFO) pass By default the level is set on the handler used to catch the log @@ -83,21 +95,21 @@ 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') + caplog.set_level(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): + with caplog.at_level(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'): + with caplog.at_level(logging.CRITICAL, logger='root.baz'): pass Lastly all the logs sent to the logger during the test run are made @@ -107,9 +119,9 @@ the contents of a message:: def test_baz(caplog): func_under_test() - for record in caplog.records(): + for record in caplog.records: assert record.levelname != 'CRITICAL' - assert 'wally' not in caplog.text() + assert 'wally' not in caplog.text For all the available attributes of the log records see the ``logging.LogRecord`` class. @@ -121,7 +133,7 @@ given severity and message:: def test_foo(caplog): logging.getLogger().info('boo %s', 'arg') - assert caplog.record_tuples() == [ + assert caplog.record_tuples == [ ('root', logging.INFO, 'boo arg'), ] diff --git a/pytest_catchlog.py b/pytest_catchlog.py index 61cdb94..892fd68 100644 --- a/pytest_catchlog.py +++ b/pytest_catchlog.py @@ -1,30 +1,104 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, print_function, - unicode_literals, division) +from __future__ import absolute_import, division, print_function +import functools import logging +from contextlib import closing, contextmanager +import pytest import py -__version__ = '1.1' +__version__ = '1.2.1' + + +def get_logger_obj(logger=None): + """Get a logger object that can be specified by its name, or passed as is. + + Defaults to the root logger. + """ + if logger is None or isinstance(logger, py.builtin._basestring): + logger = logging.getLogger(logger) + return logger + + +@contextmanager +def logging_at_level(level, logger=None): + """Context manager that sets the level for capturing of logs.""" + logger = get_logger_obj(logger) + + orig_level = logger.level + logger.setLevel(level) + try: + yield + finally: + logger.setLevel(orig_level) + + +@contextmanager +def logging_using_handler(handler, logger=None): + """Context manager that safely register a given handler.""" + logger = get_logger_obj(logger) + + if handler in logger.handlers: # reentrancy + # Adding the same handler twice would confuse logging system. + # Just don't do that. + yield + else: + logger.addHandler(handler) + try: + yield + finally: + logger.removeHandler(handler) + + +@contextmanager +def catching_logs(handler, filter=None, formatter=None, + level=logging.NOTSET, logger=None): + """Context manager that prepares the whole logging machinery properly.""" + logger = get_logger_obj(logger) + + if filter is not None: + handler.addFilter(filter) + if formatter is not None: + handler.setFormatter(formatter) + handler.setLevel(level) + + with closing(handler): + with logging_using_handler(handler, logger): + with logging_at_level(min(handler.level, logger.level), logger): + + yield handler + + +def add_option_ini(parser, option, dest, default=None, help=None): + parser.addini(dest, default=default, + help='default value for ' + option) + parser.getgroup('catchlog').addoption(option, dest=dest, help=help) + +def get_option_ini(config, name): + ret = config.getoption(name) # 'default' arg won't work as expected + if ret is None: + ret = config.getini(name) + return ret def pytest_addoption(parser): """Add options to control log capturing.""" - group = parser.getgroup('catchlog', 'Log catching.') + 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.') + add_option_ini(parser, + '--log-format', + dest='log_format', default=('%(filename)-25s %(lineno)4d' + ' %(levelname)-8s %(message)s'), + help='log format as used by the logging module.') + add_option_ini(parser, + '--log-date-format', + dest='log_date_format', default=None, + help='log date format as used by the logging module.') def pytest_configure(config): @@ -44,62 +118,44 @@ class CatchLogPlugin(object): 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')) - + self.print_logs = config.getoption('log_print') + self.formatter = logging.Formatter( + get_option_ini(config, 'log_format'), + get_option_ini(config, 'log_date_format')) + + @contextmanager + def _runtest_for(self, item, when): + """Implements the internals of pytest_runtest_xxx() hook.""" + with catching_logs(LogCaptureHandler(), + formatter=self.formatter) as log_handler: + item.catch_log_handler = log_handler + try: + yield # run test + finally: + del item.catch_log_handler + + if self.print_logs: + # Add a captured log section to the report. + log = log_handler.stream.getvalue().strip() + item.add_report_section(when, 'log', log) + + @pytest.mark.hookwrapper 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. - """ + with self._runtest_for(item, 'setup'): + yield - # Create a handler for this test. - item.catch_log_handler = CatchLogHandler() - item.catch_log_handler.setFormatter(self.formatter) + @pytest.mark.hookwrapper + def pytest_runtest_call(self, item): + with self._runtest_for(item, 'call'): + yield - # 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) + @pytest.mark.hookwrapper + def pytest_runtest_teardown(self, item): + with self._runtest_for(item, 'teardown'): + yield - 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): +class LogCaptureHandler(logging.StreamHandler): """A logging handler that stores log records and the log text.""" def __init__(self): @@ -122,24 +178,28 @@ class CatchLogHandler(logging.StreamHandler): logging.StreamHandler.emit(self, record) -class CatchLogFuncArg(object): +class LogCaptureFixture(object): """Provides access and control of log capturing.""" - def __init__(self, handler): - """Creates a new funcarg.""" + @property + def handler(self): + return self._item.catch_log_handler - self.handler = handler + def __init__(self, item): + """Creates a new funcarg.""" + self._item = item + @property def text(self): """Returns the log text.""" - return self.handler.stream.getvalue() + @property def records(self): """Returns the list of log records.""" - return self.handler.records + @property def record_tuples(self): """Returns a list of a striped down version of log records intended for use in assertion comparison. @@ -148,7 +208,7 @@ class CatchLogFuncArg(object): (logger_name, log_level, message) """ - return [(r.name, r.levelno, r.getMessage()) for r in self.records()] + 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. @@ -170,37 +230,84 @@ class CatchLogFuncArg(object): """ obj = logger and logging.getLogger(logger) or self.handler - return CatchLogLevel(obj, level) + return logging_at_level(level, obj) -class CatchLogLevel(object): - """Context manager that sets the logging level of a handler or logger.""" +class CallablePropertyMixin(object): + """Backward compatibility for functions that became properties.""" - def __init__(self, obj, level): - """Creates a new log level context manager.""" + @classmethod + def compat_property(cls, func): + if isinstance(func, property): + make_property = func.getter + func = func.fget + else: + make_property = property - self.obj = obj - self.level = level + @functools.wraps(func) + def getter(self): + naked_value = func(self) + ret = cls(naked_value) + ret._naked_value = naked_value + ret._warn_compat = self._warn_compat + ret._prop_name = func.__name__ + return ret - def __enter__(self): - """Adjust the log level.""" + return make_property(getter) - self.orig_level = self.obj.level - self.obj.setLevel(self.level) + def __call__(self): + self._warn_compat(old="'caplog.{0}()' syntax".format(self._prop_name), + new="'caplog.{0}' property".format(self._prop_name)) + return self._naked_value # to let legacy clients modify the object - def __exit__(self, exc_type, exc_value, traceback): - """Restore the log level.""" +class CallableList(CallablePropertyMixin, list): + pass - self.obj.setLevel(self.orig_level) +class CallableStr(CallablePropertyMixin, str): + pass -def pytest_funcarg__caplog(request): - """Returns a funcarg to access and control log capturing.""" +class CompatLogCaptureFixture(LogCaptureFixture): + """Backward compatibility with pytest-capturelog.""" - return CatchLogFuncArg(request._pyfuncitem.catch_log_handler) + def _warn_compat(self, old, new): + self._item.warn(code='L1', + message=("{0} is deprecated, use {1} instead" + .format(old, new))) + @CallableStr.compat_property + def text(self): + return super(CompatLogCaptureFixture, self).text + + @CallableList.compat_property + def records(self): + return super(CompatLogCaptureFixture, self).records + + @CallableList.compat_property + def record_tuples(self): + return super(CompatLogCaptureFixture, self).record_tuples + + def setLevel(self, level, logger=None): + self._warn_compat(old="'caplog.setLevel()'", + new="'caplog.set_level()'") + return self.set_level(level, logger) -def pytest_funcarg__capturelog(request): - """Returns a funcarg to access and control log capturing.""" + def atLevel(self, level, logger=None): + self._warn_compat(old="'caplog.atLevel()'", + new="'caplog.at_level()'") + return self.at_level(level, logger) + + +@pytest.fixture +def caplog(request): + """Access and control log capturing. + + Captured logs are available through the following methods:: + + * caplog.text() -> string containing formatted log output + * caplog.records() -> list of logging.LogRecord instances + * caplog.record_tuples() -> list of (logger_name, level, message) tuples + """ + return CompatLogCaptureFixture(request.node) - return CatchLogFuncArg(request._pyfuncitem.catch_log_handler) +capturelog = caplog diff --git a/setup.py b/setup.py index ba84092..0a99448 100755 --- a/setup.py +++ b/setup.py @@ -21,26 +21,29 @@ 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'), + long_description='\n'.join([_read_text_file('README.rst'), + _read_text_file('CHANGES.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', ], + install_requires=['py>=1.1.1', 'pytest>=2.6'], entry_points={'pytest11': ['pytest_catchlog = pytest_catchlog']}, license='MIT License', zip_safe=False, - keywords='py.test pytest', + keywords='py.test pytest logging', classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', '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 :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Testing']) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..e271c99 --- /dev/null +++ b/tasks.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +import os +import re +import io +from contextlib import contextmanager +from datetime import datetime + +from invoke import task, run + +VERSION_FILE = 'pytest_catchlog.py' +CHANGE_LOG_FILE = 'CHANGES.rst' + + +def _path_abs_join(*nodes): + return os.path.abspath(os.path.join(os.path.dirname(__file__), *nodes)) + + +def _path_open(*nodes, **kwargs): + return io.open(_path_abs_join(*nodes), **kwargs) + + +def _shell_quote(s): + """Quote given string to be suitable as input for bash as argument.""" + if not s: + return "''" + if re.search(r'[^\w@%+=:,./-]', s) is None: + return s + return "'" + s.replace("'", "'\"'\"'") + "'" + + +def _git_do(*commands, **kwargs): + """Execute arbitrary git commands.""" + kwargs.setdefault('hide', 'out') + results = [run('git ' + command, **kwargs).stdout.strip('\n') + for command in commands] + return results if len(commands) > 1 else results[0] + + +def _git_checkout(branch_name): + """Switches to the given branch name.""" + return _git_do('checkout ' + _shell_quote(branch_name)) + + +@contextmanager +def _git_work_on(branch_name): + """Work on given branch. Preserves current git branch.""" + original_branch = _git_do('rev-parse --abbrev-ref HEAD') + try: + if original_branch != branch_name: + _git_checkout(branch_name) + yield + finally: + if original_branch and original_branch != branch_name: + _git_checkout(original_branch) + + +def _version_find_existing(): + """Returns set of existing versions in this repository. + + This information is backed by previously used version tags + stored in the git repository. + """ + git_tags = [y.strip() for y in _git_do('tag -l').split('\n')] + + _version_re = re.compile(r'^v?(\d+)(?:\.(\d+)(?:\.(\d+))?)?$') + return {tuple(int(n) if n else 0 for n in m.groups()) + for m in (_version_re.match(t) for t in git_tags if t) if m} + + +def _version_find_latest(): + """Returns the most recent used version number. + + This information is backed by previously used version tags + stored in the git repository. + """ + return max(_version_find_existing()) + + +def _version_guess_next(position='minor'): + """Guess next version. + + A guess for the next version is determined by incrementing given + position or minor level position in latest existing version. + """ + try: + latest_version = list(_version_find_latest()) + except ValueError: + latest_version = [0, 0, 0] + + position_index = {'major': 0, 'minor': 1, 'patch': 2}[position] + latest_version[position_index] += 1 + latest_version[position_index + 1:] = [0] * (2 - position_index) + return tuple(latest_version) + + +def _version_format(version): + """Return version in dotted string format.""" + return '.'.join(str(x) for x in version) + + +def _patch_file(file_path, line_callback): + """Patch given file with result from line callback. + + Each line will be passed to the line callback. + The return value of the given callback will determine + the new content for the file. + + :param str file_path: + The file to patch. + :param callable line_callback: + The patch function to run over each line. + :return: + Whenever the file has changed or not. + :rtype: + bool + """ + new_file_content, file_changed = [], False + with _path_open(file_path) as in_stream: + for l in (x.strip('\n') for x in in_stream): + alt_lines = line_callback(l) or [l] + if alt_lines != [l]: + file_changed = True + new_file_content += (x + u'\n' for x in alt_lines) + + new_file_name = file_path + '.new' + with _path_open(new_file_name, mode='w') as out_stream: + out_stream.writelines(new_file_content) + out_stream.flush() + os.fsync(out_stream.fileno()) + os.rename(new_file_name, file_path) + + return file_changed + + +def _patch_version(new_version): + """Patch given version into version file.""" + _patch_version_re = re.compile(r"""^(\s*__version__\s*=\s*(?:"|'))""" + r"""(?:[^'"]*)(?:("|')\s*)$""") + + def __line_callback(line): + match = _patch_version_re.match(line) + if match: + line_head, line_tail = match.groups() + return [line_head + new_version + line_tail] + return _patch_file(VERSION_FILE, __line_callback) + + +def _patch_change_log(new_version): + """Patch given version into change log file.""" + def __line_callback(line): + if line == u'`Unreleased`_': + return [u'`{}`_'.format(new_version)] + elif line == u'Yet to be released.': + return [datetime.utcnow().strftime(u'Released on %F.')] + elif line == u'.. %UNRELEASED_SECTION%': + return [u'.. %UNRELEASED_SECTION%', + u'', + u'`Unreleased`_', + u'-------------', + u'', + u'Yet to be released.', + u''] + return _patch_file(CHANGE_LOG_FILE, __line_callback) + + +@task() +def mkrelease(position='minor'): + """Merge development state into Master Branch and tags a new Release.""" + next_version = _version_format(_version_guess_next(position)) + with _git_work_on('develop'): + patched_files = [] + if _patch_version(next_version): + patched_files.append(VERSION_FILE) + + if _patch_change_log(next_version): + patched_files.append(CHANGE_LOG_FILE) + + if patched_files: + patched_files = ' '.join(_shell_quote(x) for x in patched_files) + _git_do('diff --color=always -- ' + patched_files, + ("commit -m 'Bump Version to {0}' -- {1}" + .format(next_version, patched_files)), + hide=None) + + with _git_work_on('master'): + message = _shell_quote('Release {0}'.format(next_version)) + _git_do('merge --no-ff --no-edit -m {0} develop'.format(message), + "tag -a -m {0} {1}".format(message, next_version)) diff --git a/test_pytest_catchlog.py b/test_pytest_catchlog.py index 0e9d55f..b7d6b3b 100644 --- a/test_pytest_catchlog.py +++ b/test_pytest_catchlog.py @@ -1,6 +1,6 @@ import py -pytest_plugins = 'pytester', 'catchlog' +pytest_plugins = 'pytester' def test_nothing_logged(testdir): @@ -8,8 +8,6 @@ def test_nothing_logged(testdir): import sys import logging - pytest_plugins = 'catchlog' - def test_foo(): sys.stdout.write('text going to stdout') sys.stderr.write('text going to stderr') @@ -22,7 +20,7 @@ def test_nothing_logged(testdir): result.stdout.fnmatch_lines(['*- Captured stderr call -*', 'text going to stderr']) py.test.raises(Exception, result.stdout.fnmatch_lines, - ['*- Captured log -*']) + ['*- Captured *log call -*']) def test_messages_logged(testdir): @@ -30,8 +28,6 @@ def test_messages_logged(testdir): import sys import logging - pytest_plugins = 'catchlog' - def test_foo(): sys.stdout.write('text going to stdout') sys.stderr.write('text going to stderr') @@ -40,7 +36,7 @@ def test_messages_logged(testdir): ''') result = testdir.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines(['*- Captured log -*', + result.stdout.fnmatch_lines(['*- Captured *log call -*', '*text going to logger*']) result.stdout.fnmatch_lines(['*- Captured stdout call -*', 'text going to stdout']) @@ -48,12 +44,50 @@ def test_messages_logged(testdir): 'text going to stderr']) -def test_change_level(testdir): +def test_setup_logging(testdir): + testdir.makepyfile(''' + import sys + import logging + + def setup_function(function): + logging.getLogger().info('text going to logger from setup') + + def test_foo(): + logging.getLogger().info('text going to logger from call') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured *log setup -*', + '*text going to logger from setup*', + '*- Captured *log call -*', + '*text going to logger from call*']) + + +def test_teardown_logging(testdir): testdir.makepyfile(''' import sys import logging - pytest_plugins = 'catchlog' + def test_foo(): + logging.getLogger().info('text going to logger from call') + + def teardown_function(function): + logging.getLogger().info('text going to logger from teardown') + assert False + ''') + result = testdir.runpytest() + assert result.ret == 1 + result.stdout.fnmatch_lines(['*- Captured *log call -*', + '*text going to logger from call*', + '*- Captured *log teardown -*', + '*text going to logger from teardown*']) + + +def test_change_level(testdir): + testdir.makepyfile(''' + import sys + import logging def test_foo(caplog): caplog.set_level(logging.INFO) @@ -70,13 +104,13 @@ def test_change_level(testdir): ''') result = testdir.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines(['*- Captured log -*', + result.stdout.fnmatch_lines(['*- Captured *log call -*', '*handler INFO level*', '*logger CRITICAL level*']) py.test.raises(Exception, result.stdout.fnmatch_lines, - ['*- Captured log -*', '*handler DEBUG level*']) + ['*- Captured *log call -*', '*handler DEBUG level*']) py.test.raises(Exception, result.stdout.fnmatch_lines, - ['*- Captured log -*', '*logger WARNING level*']) + ['*- Captured *log call -*', '*logger WARNING level*']) @py.test.mark.skipif('sys.version_info < (2,5)') @@ -86,8 +120,6 @@ def test_with_statement(testdir): import sys import logging - pytest_plugins = 'catchlog' - def test_foo(caplog): with caplog.at_level(logging.INFO): log = logging.getLogger() @@ -103,13 +135,13 @@ def test_with_statement(testdir): ''') result = testdir.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines(['*- Captured log -*', + result.stdout.fnmatch_lines(['*- Captured *log call -*', '*handler INFO level*', '*logger CRITICAL level*']) py.test.raises(Exception, result.stdout.fnmatch_lines, - ['*- Captured log -*', '*handler DEBUG level*']) + ['*- Captured *log call -*', '*handler DEBUG level*']) py.test.raises(Exception, result.stdout.fnmatch_lines, - ['*- Captured log -*', '*logger WARNING level*']) + ['*- Captured *log call -*', '*logger WARNING level*']) def test_log_access(testdir): @@ -117,13 +149,11 @@ def test_log_access(testdir): 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() + 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 @@ -139,12 +169,10 @@ def test_record_tuples(testdir): import sys import logging - pytest_plugins = 'catchlog' - def test_foo(caplog): logging.getLogger().info('boo %s', 'arg') - assert caplog.record_tuples() == [ + assert caplog.record_tuples == [ ('root', logging.INFO, 'boo arg'), ] ''') @@ -152,13 +180,85 @@ def test_record_tuples(testdir): assert result.ret == 0 +def test_compat_camel_case_aliases(testdir): + testdir.makepyfile(''' + import logging + + def test_foo(caplog): + caplog.setLevel(logging.INFO) + logging.getLogger().debug('boo!') + + with caplog.atLevel(logging.WARNING): + logging.getLogger().info('catch me if you can') + ''') + result = testdir.runpytest() + assert result.ret == 0 + + py.test.raises(Exception, result.stdout.fnmatch_lines, + ['*- Captured *log call -*']) + + result = testdir.runpytest('-rw') + assert result.ret == 0 + result.stdout.fnmatch_lines(''' + =*warning summary*= + *WL1*test_compat_camel_case_aliases*caplog.setLevel()*deprecated* + *WL1*test_compat_camel_case_aliases*caplog.atLevel()*deprecated* + ''') + + +def test_compat_properties(testdir): + testdir.makepyfile(''' + import logging + + def test_foo(caplog): + logging.getLogger().info('boo %s', 'arg') + + assert caplog.text == caplog.text() == str(caplog.text) + assert caplog.records == caplog.records() == list(caplog.records) + assert (caplog.record_tuples == + caplog.record_tuples() == list(caplog.record_tuples)) + ''') + result = testdir.runpytest() + assert result.ret == 0 + + result = testdir.runpytest('-rw') + assert result.ret == 0 + result.stdout.fnmatch_lines(''' + =*warning summary*= + *WL1*test_compat_properties*caplog.text()*deprecated* + *WL1*test_compat_properties*caplog.records()*deprecated* + *WL1*test_compat_properties*caplog.record_tuples()*deprecated* + ''') + + +def test_compat_records_modification(testdir): + testdir.makepyfile(''' + import logging + + logger = logging.getLogger() + + def test_foo(caplog): + logger.info('boo %s', 'arg') + assert caplog.records + assert caplog.records() + + del caplog.records()[:] # legacy syntax + assert not caplog.records + assert not caplog.records() + + logger.info('foo %s', 'arg') + assert caplog.records + assert caplog.records() + ''') + 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!') @@ -173,4 +273,4 @@ def test_disable_log_capturing(testdir): result.stdout.fnmatch_lines(['*- Captured stderr call -*', 'text going to stderr']) py.test.raises(Exception, result.stdout.fnmatch_lines, - ['*- Captured log -*']) + ['*- Captured *log call -*']) diff --git a/tox.ini b/tox.ini index 3be9fcb..ab154e2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,32,33,34,py,py3} +envlist = py{26,27,32,33,34,35}, pypy{,3} [testenv] deps = -- cgit v1.2.3