Add --log-file-mode option to the logging plugin, enabling appending to log-files (#11979)
Previously, the mode was hard-coded to be "w" which truncates the file before logging. Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
This commit is contained in:
parent
8a410d0ba6
commit
c5c729e27a
1
AUTHORS
1
AUTHORS
|
@ -56,6 +56,7 @@ Babak Keyvani
|
||||||
Barney Gale
|
Barney Gale
|
||||||
Ben Brown
|
Ben Brown
|
||||||
Ben Gartner
|
Ben Gartner
|
||||||
|
Ben Leith
|
||||||
Ben Webb
|
Ben Webb
|
||||||
Benjamin Peterson
|
Benjamin Peterson
|
||||||
Benjamin Schubert
|
Benjamin Schubert
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
|
||||||
|
|
||||||
|
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
|
|
@ -206,8 +206,9 @@ option names are:
|
||||||
* ``log_cli_date_format``
|
* ``log_cli_date_format``
|
||||||
|
|
||||||
If you need to record the whole test suite logging calls to a file, you can pass
|
If you need to record the whole test suite logging calls to a file, you can pass
|
||||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
|
||||||
means that it will be overwritten at each run tests session.
|
means that it will be overwritten at each run tests session.
|
||||||
|
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
|
||||||
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
||||||
config file, are always resolved relative to the current working directory.
|
config file, are always resolved relative to the current working directory.
|
||||||
|
|
||||||
|
@ -223,12 +224,13 @@ All of the log file options can also be set in the configuration INI file. The
|
||||||
option names are:
|
option names are:
|
||||||
|
|
||||||
* ``log_file``
|
* ``log_file``
|
||||||
|
* ``log_file_mode``
|
||||||
* ``log_file_level``
|
* ``log_file_level``
|
||||||
* ``log_file_format``
|
* ``log_file_format``
|
||||||
* ``log_file_date_format``
|
* ``log_file_date_format``
|
||||||
|
|
||||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
||||||
is considered **experimental**.
|
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
|
||||||
|
|
||||||
.. _log_colors:
|
.. _log_colors:
|
||||||
|
|
||||||
|
|
|
@ -298,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
default=None,
|
default=None,
|
||||||
help="Path to a file when logging will be written to",
|
help="Path to a file when logging will be written to",
|
||||||
)
|
)
|
||||||
|
add_option_ini(
|
||||||
|
"--log-file-mode",
|
||||||
|
dest="log_file_mode",
|
||||||
|
default="w",
|
||||||
|
choices=["w", "a"],
|
||||||
|
help="Log file open mode",
|
||||||
|
)
|
||||||
add_option_ini(
|
add_option_ini(
|
||||||
"--log-file-level",
|
"--log-file-level",
|
||||||
dest="log_file_level",
|
dest="log_file_level",
|
||||||
|
@ -669,7 +676,10 @@ class LoggingPlugin:
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
os.makedirs(directory)
|
os.makedirs(directory)
|
||||||
|
|
||||||
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
|
self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
|
||||||
|
self.log_file_handler = _FileHandler(
|
||||||
|
log_file, mode=self.log_file_mode, encoding="UTF-8"
|
||||||
|
)
|
||||||
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||||
log_file_date_format = get_option_ini(
|
log_file_date_format = get_option_ini(
|
||||||
config, "log_file_date_format", "log_date_format"
|
config, "log_file_date_format", "log_date_format"
|
||||||
|
@ -746,7 +756,7 @@ class LoggingPlugin:
|
||||||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
# https://github.com/python/mypy/issues/11193
|
# https://github.com/python/mypy/issues/11193
|
||||||
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
|
stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
|
||||||
old_stream = self.log_file_handler.setStream(stream)
|
old_stream = self.log_file_handler.setStream(stream)
|
||||||
if old_stream:
|
if old_stream:
|
||||||
old_stream.close()
|
old_stream.close()
|
||||||
|
|
|
@ -661,6 +661,73 @@ def test_log_file_cli(pytester: Pytester) -> None:
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_file_mode_cli(pytester: Pytester) -> None:
|
||||||
|
# Default log file level
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
def test_log_file(request):
|
||||||
|
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||||
|
assert plugin.log_file_handler.level == logging.WARNING
|
||||||
|
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||||
|
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||||
|
print('PASSED')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
with open(log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header\n")
|
||||||
|
|
||||||
|
result = pytester.runpytest(
|
||||||
|
"-s",
|
||||||
|
f"--log-file={log_file}",
|
||||||
|
"--log-file-mode=a",
|
||||||
|
"--log-file-level=WARNING",
|
||||||
|
)
|
||||||
|
|
||||||
|
# fnmatch_lines does an assertion internally
|
||||||
|
result.stdout.fnmatch_lines(["test_log_file_mode_cli.py PASSED"])
|
||||||
|
|
||||||
|
# make sure that we get a '0' exit code for the testsuite
|
||||||
|
assert result.ret == 0
|
||||||
|
assert os.path.isfile(log_file)
|
||||||
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
|
contents = rfh.read()
|
||||||
|
assert "A custom header" in contents
|
||||||
|
assert "This log message will be shown" in contents
|
||||||
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_file_mode_cli_invalid(pytester: Pytester) -> None:
|
||||||
|
# Default log file level
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
def test_log_file(request):
|
||||||
|
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||||
|
assert plugin.log_file_handler.level == logging.WARNING
|
||||||
|
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||||
|
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
result = pytester.runpytest(
|
||||||
|
"-s",
|
||||||
|
f"--log-file={log_file}",
|
||||||
|
"--log-file-mode=b",
|
||||||
|
"--log-file-level=WARNING",
|
||||||
|
)
|
||||||
|
|
||||||
|
# make sure that we get a '4' exit code for the testsuite
|
||||||
|
assert result.ret == ExitCode.USAGE_ERROR
|
||||||
|
|
||||||
|
|
||||||
def test_log_file_cli_level(pytester: Pytester) -> None:
|
def test_log_file_cli_level(pytester: Pytester) -> None:
|
||||||
# Default log file level
|
# Default log file level
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
|
@ -741,6 +808,47 @@ def test_log_file_ini(pytester: Pytester) -> None:
|
||||||
assert "This log message won't be shown" not in contents
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_file_mode_ini(pytester: Pytester) -> None:
|
||||||
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
pytester.makeini(
|
||||||
|
f"""
|
||||||
|
[pytest]
|
||||||
|
log_file={log_file}
|
||||||
|
log_file_mode=a
|
||||||
|
log_file_level=WARNING
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
import logging
|
||||||
|
def test_log_file(request):
|
||||||
|
plugin = request.config.pluginmanager.getplugin('logging-plugin')
|
||||||
|
assert plugin.log_file_handler.level == logging.WARNING
|
||||||
|
logging.getLogger('catchlog').info("This log message won't be shown")
|
||||||
|
logging.getLogger('catchlog').warning("This log message will be shown")
|
||||||
|
print('PASSED')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header\n")
|
||||||
|
|
||||||
|
result = pytester.runpytest("-s")
|
||||||
|
|
||||||
|
# fnmatch_lines does an assertion internally
|
||||||
|
result.stdout.fnmatch_lines(["test_log_file_mode_ini.py PASSED"])
|
||||||
|
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
assert os.path.isfile(log_file)
|
||||||
|
with open(log_file, encoding="utf-8") as rfh:
|
||||||
|
contents = rfh.read()
|
||||||
|
assert "A custom header" in contents
|
||||||
|
assert "This log message will be shown" in contents
|
||||||
|
assert "This log message won't be shown" not in contents
|
||||||
|
|
||||||
|
|
||||||
def test_log_file_ini_level(pytester: Pytester) -> None:
|
def test_log_file_ini_level(pytester: Pytester) -> None:
|
||||||
log_file = str(pytester.path.joinpath("pytest.log"))
|
log_file = str(pytester.path.joinpath("pytest.log"))
|
||||||
|
|
||||||
|
@ -1060,6 +1168,66 @@ def test_log_set_path(pytester: Pytester) -> None:
|
||||||
assert "message from test 2" in content
|
assert "message from test 2" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_set_path_with_log_file_mode(pytester: Pytester) -> None:
|
||||||
|
report_dir_base = str(pytester.path)
|
||||||
|
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
log_file_level = DEBUG
|
||||||
|
log_cli=true
|
||||||
|
log_file_mode=a
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makeconftest(
|
||||||
|
f"""
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
config = item.config
|
||||||
|
logging_plugin = config.pluginmanager.get_plugin("logging-plugin")
|
||||||
|
report_file = os.path.join({report_dir_base!r}, item._request.node.name)
|
||||||
|
logging_plugin.set_log_path(report_file)
|
||||||
|
return (yield)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.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
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
test_first_log_file = os.path.join(report_dir_base, "test_first")
|
||||||
|
test_second_log_file = os.path.join(report_dir_base, "test_second")
|
||||||
|
with open(test_first_log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header for test 1\n")
|
||||||
|
|
||||||
|
with open(test_second_log_file, mode="w", encoding="utf-8") as wfh:
|
||||||
|
wfh.write("A custom header for test 2\n")
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
|
||||||
|
with open(test_first_log_file, encoding="utf-8") as rfh:
|
||||||
|
content = rfh.read()
|
||||||
|
assert "A custom header for test 1" in content
|
||||||
|
assert "message from test 1" in content
|
||||||
|
|
||||||
|
with open(test_second_log_file, encoding="utf-8") as rfh:
|
||||||
|
content = rfh.read()
|
||||||
|
assert "A custom header for test 2" in content
|
||||||
|
assert "message from test 2" in content
|
||||||
|
|
||||||
|
|
||||||
def test_colored_captured_log(pytester: Pytester) -> None:
|
def test_colored_captured_log(pytester: Pytester) -> None:
|
||||||
"""Test that the level names of captured log messages of a failing test
|
"""Test that the level names of captured log messages of a failing test
|
||||||
are colored."""
|
are colored."""
|
||||||
|
|
Loading…
Reference in New Issue