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
|
||||
Ben Brown
|
||||
Ben Gartner
|
||||
Ben Leith
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
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``
|
||||
|
||||
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.
|
||||
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
|
||||
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:
|
||||
|
||||
* ``log_file``
|
||||
* ``log_file_mode``
|
||||
* ``log_file_level``
|
||||
* ``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**.
|
||||
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
|
||||
|
||||
.. _log_colors:
|
||||
|
||||
|
|
|
@ -298,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
|
|||
default=None,
|
||||
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(
|
||||
"--log-file-level",
|
||||
dest="log_file_level",
|
||||
|
@ -669,7 +676,10 @@ class LoggingPlugin:
|
|||
if not os.path.isdir(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_date_format = get_option_ini(
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
|
@ -746,7 +756,7 @@ class LoggingPlugin:
|
|||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# 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)
|
||||
if old_stream:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
# Default log file level
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""Test that the level names of captured log messages of a failing test
|
||||
are colored."""
|
||||
|
|
Loading…
Reference in New Issue