2 new commits in pytest: https://bitbucket.org/hpk42/pytest/commits/dfff965c16cb/ Changeset: dfff965c16cb Branch: copy-in-cache User: RonnyPfannschmidt Date: 2015-02-19 18:04:06+00:00 Summary: just copy in capturelog with minimal changes Affected #: 4 files
diff -r bc2cf073d49b5eaa8a403c2e1409e98f450d2ea5 -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 _pytest/capture_log.py --- /dev/null +++ b/_pytest/capture_log.py @@ -0,0 +1,301 @@ +"""capture output of logging module. + +Installation +------------ + +The `pytest-capturelog`_ package may be installed with pip or easy_install:: + + pip install pytest-capturelog + easy_install pytest-capturelog + +.. _`pytest-capturelog`: http://pypi.python.org/pypi/pytest-capturelog/ + +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_capturelog.py + +Shows failed tests like so:: + + -------------------------- Captured log --------------------------- + test_capturelog.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_capturelog.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 capturing of logs completely with:: + + py.test --nocapturelog test_capturelog.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 capture 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. +""" + +import py +import logging + +LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s' + + +def pytest_addoption(parser): + """Add options to control log capturing.""" + + group = parser.getgroup('capturelog', 'log capturing') + group.addoption('--nocapturelog', + dest='capturelog', + action='store_false', + default=True, + help='disable log capture') + group.addoption('--log-format', + dest='log_format', + default=LOG_FORMAT, + 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): + """Activate log capturing if appropriate.""" + + if config.getvalue('capturelog'): + config.pluginmanager.register(CaptureLogPlugin(config), '_capturelog') + + +class CaptureLogPlugin(object): + """Attaches to the logging module and captures log messages for each test.""" + + def __init__(self, config): + """Creates a new plugin to capture log messges. + + The formatter can be safely shared across all handlers so + create a single one for the entire test session here. + """ + + 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.capturelog_handler = CaptureLogHandler() + item.capturelog_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.capturelog_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.capturelog_handler) + + # For failed tests that have captured log messages add a + # captured log section to the report. + if not report.passed: + longrepr = getattr(report, 'longrepr', None) + if hasattr(longrepr, 'addsection'): + log = item.capturelog_handler.stream.getvalue().strip() + if log: + longrepr.addsection('Captured log', log) + + # Release the handler resources. + item.capturelog_handler.close() + del item.capturelog_handler + + return report + + +class CaptureLogHandler(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 CaptureLogFuncArg(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 setLevel(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 atLevel(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 CaptureLogLevel(obj, level) + + +class CaptureLogLevel(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 CaptureLogFuncArg(request._pyfuncitem.capturelog_handler) + + +def pytest_funcarg__capturelog(request): + """Returns a funcarg to access and control log capturing.""" + + return CaptureLogFuncArg(request._pyfuncitem.capturelog_handler) diff -r bc2cf073d49b5eaa8a403c2e1409e98f450d2ea5 -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 _pytest/config.py --- a/_pytest/config.py +++ b/_pytest/config.py @@ -51,7 +51,7 @@ default_plugins = ( "mark main terminal runner python pdb unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml resultlog doctest cache").split() + "junitxml resultlog doctest cache capture_log").split() def _preloadplugins(): assert not _preinit diff -r bc2cf073d49b5eaa8a403c2e1409e98f450d2ea5 -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 testing/test_capture.py --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -337,6 +337,7 @@ # verify proper termination assert "closed" not in s + @pytest.mark.xfail(reason='logcapture integration') def test_logging_initialized_in_test(self, testdir): p = testdir.makepyfile(""" import sys diff -r bc2cf073d49b5eaa8a403c2e1409e98f450d2ea5 -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 testing/test_capturelog.py --- /dev/null +++ b/testing/test_capturelog.py @@ -0,0 +1,106 @@ +import py + +def test_nothing_logged(testdir): + testdir.makepyfile(''' + import sys + import logging + + + 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 *', 'text going to stdout']) + result.stdout.fnmatch_lines(['* Captured stderr *', '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 + + + 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 *', 'text going to stdout']) + result.stdout.fnmatch_lines(['* Captured stderr *', 'text going to stderr']) + +def test_change_level(testdir): + testdir.makepyfile(''' + import sys + import logging + + + def test_foo(caplog): + caplog.setLevel(logging.INFO) + log = logging.getLogger() + log.debug('handler DEBUG level') + log.info('handler INFO level') + + caplog.setLevel(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_with_statement(testdir): + testdir.makepyfile(''' + from __future__ import with_statement + import sys + import logging + + + def test_foo(caplog): + with caplog.atLevel(logging.INFO): + log = logging.getLogger() + log.debug('handler DEBUG level') + log.info('handler INFO level') + + with caplog.atLevel(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 + + + 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*']) https://bitbucket.org/hpk42/pytest/commits/8211b20606ae/ Changeset: 8211b20606ae Branch: copy-in-cache User: RonnyPfannschmidt Date: 2015-02-19 20:29:52+00:00 Summary: ignore the pytest-cache/capturelog distributions and fix related tests Affected #: 4 files diff -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 -r 8211b20606ae19bc1b71144e623c9bd5964773f2 _pytest/core.py --- a/_pytest/core.py +++ b/_pytest/core.py @@ -10,7 +10,11 @@ assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) -py3 = sys.version_info > (3,0) +py3 = sys.version_info > (3, 0) + +# +_ALREADY_INCLUDED_PLUGINS = 'pytest-cache', 'pytest-capturelog' + class TagTracer: def __init__(self): @@ -269,6 +273,11 @@ except ImportError: return # XXX issue a warning for ep in iter_entry_points('pytest11'): + project_name = getattr(ep.dist, 'project_name', None) + if project_name in _ALREADY_INCLUDED_PLUGINS: + self._warnings.append( + 'ignoring now included plugin ' + project_name) + continue name = ep.name if name.startswith("pytest_"): name = name[7:] diff -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 -r 8211b20606ae19bc1b71144e623c9bd5964773f2 testing/test_config.py --- a/testing/test_config.py +++ b/testing/test_config.py @@ -307,6 +307,7 @@ assert name == "pytest11" class EntryPoint: name = "mytestplugin" + dist = None def load(self): assert 0, "should not arrive here" return iter([EntryPoint()]) diff -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 -r 8211b20606ae19bc1b71144e623c9bd5964773f2 testing/test_core.py --- a/testing/test_core.py +++ b/testing/test_core.py @@ -70,12 +70,13 @@ assert name == "pytest11" class EntryPoint: name = "pytest_mytestplugin" - dist = None + class dist: + name = None def load(self): class PseudoPlugin: x = 42 return PseudoPlugin() - return iter([EntryPoint()]) + yield EntryPoint() monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) pluginmanager = PluginManager() @@ -83,6 +84,28 @@ plugin = pluginmanager.getplugin("mytestplugin") assert plugin.x == 42 + @pytest.mark.parametrize('distname', ['pytest-cache', 'pytest-capturelog']) + def test_dont_consider_setuptools_included(self, distname, monkeypatch): + pkg_resources = pytest.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "pytest_mytestplugin" + class dist: + project_name = distname + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + yield EntryPoint() + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pluginmanager = PluginManager() + pluginmanager.consider_setuptools_entrypoints() + plugin = pluginmanager.getplugin("mytestplugin") + assert plugin is None + + def test_consider_setuptools_not_installed(self, monkeypatch): monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', py.std.types.ModuleType("pkg_resources")) diff -r dfff965c16cb8ad509b0cd16e2930081d628dbc1 -r 8211b20606ae19bc1b71144e623c9bd5964773f2 tox.ini --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,9 @@ [testenv:flakes] changedir= -deps = pytest-flakes>=0.2 -commands = py.test --flakes -m flakes _pytest testing +deps = flake8 + mccabe +commands = pytest.py _pytest testing [testenv:py27-xdist] changedir=. @@ -141,5 +142,8 @@ python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance python_functions=test -pep8ignore = E401 E225 E261 E128 E124 E302 norecursedirs = .tox ja .hg + + +[flake8] +ignore = E401 E225 E261 E128 E124 E302 Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. _______________________________________________ pytest-commit mailing list pytest-commit@python.org https://mail.python.org/mailman/listinfo/pytest-commit