diff --git a/AUTHORS b/AUTHORS index 48f73f7a2..85fe6aff0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Allan Feldman Aly Sivji Anatoly Bubenkoff Anders Hovmöller +Andras Mitzki Andras Tim Andrea Cimatoribus Andreas Zeidler diff --git a/changelog/4707.feature.rst b/changelog/4707.feature.rst new file mode 100644 index 000000000..20c387eb9 --- /dev/null +++ b/changelog/4707.feature.rst @@ -0,0 +1 @@ +With the help of new ``set_log_path()`` method there is a way to set ``log_file`` paths from hooks. diff --git a/doc/en/logging.rst b/doc/en/logging.rst index 00829c15e..197528d7c 100644 --- a/doc/en/logging.rst +++ b/doc/en/logging.rst @@ -198,6 +198,9 @@ option names are: * ``log_file_format`` * ``log_file_date_format`` +You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality +is considered **experimental**. + .. _log_release_notes: Release notes diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 343d4dd1c..5234b5b8a 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -13,6 +13,7 @@ import six import pytest from _pytest.compat import dummy_context_manager from _pytest.config import create_terminal_writer +from _pytest.pathlib import Path DEFAULT_LOG_FORMAT = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" @@ -399,22 +400,21 @@ class LoggingPlugin(object): ) self.log_level = get_actual_log_level(config, "log_level") + self.log_file_level = get_actual_log_level(config, "log_file_level") + self.log_file_format = get_option_ini(config, "log_file_format", "log_format") + self.log_file_date_format = get_option_ini( + config, "log_file_date_format", "log_date_format" + ) + self.log_file_formatter = logging.Formatter( + self.log_file_format, datefmt=self.log_file_date_format + ) + log_file = get_option_ini(config, "log_file") if log_file: - self.log_file_level = get_actual_log_level(config, "log_file_level") - - log_file_format = get_option_ini(config, "log_file_format", "log_format") - log_file_date_format = get_option_ini( - config, "log_file_date_format", "log_date_format" - ) - # Each pytest runtests session will write to a clean logfile self.log_file_handler = logging.FileHandler( log_file, mode="w", encoding="UTF-8" ) - log_file_formatter = logging.Formatter( - log_file_format, datefmt=log_file_date_format - ) - self.log_file_handler.setFormatter(log_file_formatter) + self.log_file_handler.setFormatter(self.log_file_formatter) else: self.log_file_handler = None @@ -461,6 +461,27 @@ class LoggingPlugin(object): log_cli_handler, formatter=log_cli_formatter, level=log_cli_level ) + def set_log_path(self, fname): + """Public method, which can set filename parameter for + Logging.FileHandler(). Also creates parent directory if + it does not exist. + + .. warning:: + Please considered as an experimental API. + """ + fname = Path(fname) + + if not fname.is_absolute(): + fname = Path(self._config.rootdir, fname) + + if not fname.parent.exists(): + fname.parent.mkdir(exist_ok=True, parents=True) + + self.log_file_handler = logging.FileHandler( + str(fname), mode="w", encoding="UTF-8" + ) + self.log_file_handler.setFormatter(self.log_file_formatter) + def _log_cli_enabled(self): """Return True if log_cli should be considered enabled, either explicitly or because --log-cli-level was given in the command-line. @@ -483,6 +504,15 @@ class LoggingPlugin(object): @contextmanager def _runtest_for(self, item, when): + with self._runtest_for_main(item, when): + if self.log_file_handler is not None: + with catching_logs(self.log_file_handler, level=self.log_file_level): + yield + else: + yield + + @contextmanager + def _runtest_for_main(self, item, when): """Implements the internals of pytest_runtest_xxx() hook.""" with catching_logs( LogCaptureHandler(), formatter=self.formatter, level=self.log_level diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 9debc2165..afeccfcc5 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1002,3 +1002,51 @@ def test_log_in_hooks(testdir): assert "sessionstart" in contents assert "runtestloop" in contents assert "sessionfinish" in contents + + +def test_log_set_path(testdir): + report_dir_base = testdir.tmpdir.strpath + + testdir.makeini( + """ + [pytest] + log_file_level = DEBUG + log_cli=true + """ + ) + testdir.makeconftest( + """ + import os + import pytest + @pytest.hookimpl(hookwrapper=True, tryfirst=True) + def pytest_runtest_setup(item): + config = item.config + logging_plugin = config.pluginmanager.get_plugin("logging-plugin") + report_file = os.path.join({}, item._request.node.name) + logging_plugin.set_log_path(report_file) + yield + """.format( + repr(report_dir_base) + ) + ) + testdir.makepyfile( + """ + import logging + logger = logging.getLogger("testcase-logger") + def test_first(): + logger.info("message from test 1") + assert True + + def test_second(): + logger.debug("message from test 2") + assert True + """ + ) + testdir.runpytest() + with open(os.path.join(report_dir_base, "test_first"), "r") as rfh: + content = rfh.read() + assert "message from test 1" in content + + with open(os.path.join(report_dir_base, "test_second"), "r") as rfh: + content = rfh.read() + assert "message from test 2" in content