A silent skip would let an unmarked file through, which is the one thing the test exists to catch. The two new changelog lines now read in the file's own voice, and a docstring in the logging tests is wrapped under the line limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
111 lines
4.2 KiB
Python
111 lines
4.2 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Tests for the log file: plain text, bounded, and holding the things that went wrong.
|
|
|
|
The log's one real job is being opened by a person who has hit a problem — often the
|
|
person reporting it. Every assertion here is about that reader.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from rich.console import Console
|
|
|
|
from meshterm.persistence.logging import (
|
|
LOG_FILENAME,
|
|
configure_logging,
|
|
get_logger,
|
|
level_from_name,
|
|
log_path,
|
|
)
|
|
|
|
|
|
def _read(log_dir: Path) -> str:
|
|
for handler in get_logger().handlers:
|
|
handler.flush()
|
|
return log_path(log_dir).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_the_log_is_plain_text(tmp_path: Path) -> None:
|
|
"""One readable line per record: when, how bad, who said it, and what."""
|
|
configure_logging(Console(quiet=True), tmp_path, file_level=logging.WARNING, quiet=True)
|
|
get_logger("core.connection").warning("link lost, retrying")
|
|
line = _read(tmp_path).splitlines()[0]
|
|
assert "WARNING" in line
|
|
assert "meshterm.core.connection" in line
|
|
assert "link lost, retrying" in line
|
|
# A date as well as a time. "it happened at 14:40" is useless a week later.
|
|
assert line.startswith("20")
|
|
|
|
|
|
def test_the_file_is_named_plainly(tmp_path: Path) -> None:
|
|
"""``meshterm.log`` — an extension every text editor already opens."""
|
|
assert log_path(tmp_path).name == LOG_FILENAME == "meshterm.log"
|
|
|
|
|
|
def test_the_level_keeps_the_quiet_records_out(tmp_path: Path) -> None:
|
|
"""At the default, the file holds problems rather than a narration of a working run.
|
|
|
|
This is the whole point of the default being WARNING: a log where the dozen useful
|
|
lines sit under twenty thousand dull ones is one nobody reads.
|
|
"""
|
|
configure_logging(Console(quiet=True), tmp_path, file_level=logging.WARNING, quiet=True)
|
|
log = get_logger()
|
|
log.debug("opened a screen")
|
|
log.info("connected to a radio")
|
|
log.warning("something went wrong")
|
|
written = _read(tmp_path)
|
|
assert "something went wrong" in written
|
|
assert "opened a screen" not in written
|
|
assert "connected to a radio" not in written
|
|
|
|
|
|
def test_turning_it_up_lets_everything_through(tmp_path: Path) -> None:
|
|
"""DEBUG is there for chasing something, and keeps what WARNING drops."""
|
|
configure_logging(Console(quiet=True), tmp_path, file_level=logging.DEBUG, quiet=True)
|
|
get_logger().debug("opened a screen")
|
|
assert "opened a screen" in _read(tmp_path)
|
|
|
|
|
|
def test_a_traceback_goes_in_with_the_record(tmp_path: Path) -> None:
|
|
"""An exception's traceback is written under the record that reports it.
|
|
|
|
This is most of why the file is worth attaching to a bug report: the terminal scrolls
|
|
away and gets closed, and this copy does not.
|
|
"""
|
|
configure_logging(Console(quiet=True), tmp_path, file_level=logging.WARNING, quiet=True)
|
|
try:
|
|
raise ValueError("the device said something impossible")
|
|
except ValueError:
|
|
get_logger().exception("tool raised")
|
|
written = _read(tmp_path)
|
|
assert "tool raised" in written
|
|
assert "ValueError: the device said something impossible" in written
|
|
assert "Traceback (most recent call last)" in written
|
|
|
|
|
|
def test_the_file_is_bounded(tmp_path: Path) -> None:
|
|
"""It rotates. An unbounded log reached 11MB in ten weeks on a real install."""
|
|
configure_logging(Console(quiet=True), tmp_path, file_level=logging.WARNING, quiet=True)
|
|
handler = next(h for h in get_logger().handlers if hasattr(h, "maxBytes"))
|
|
assert handler.maxBytes > 0
|
|
assert handler.backupCount > 0
|
|
|
|
|
|
def test_a_level_name_becomes_a_level(tmp_path: Path) -> None:
|
|
"""The preference stores a name; the handler needs a number."""
|
|
assert level_from_name("DEBUG") == logging.DEBUG
|
|
assert level_from_name("warning") == logging.WARNING
|
|
assert level_from_name(" Error ") == logging.ERROR
|
|
|
|
|
|
def test_a_nonsense_level_still_starts_the_app() -> None:
|
|
"""A typo in a hand-edited preferences file loses log detail, not the app.
|
|
|
|
Someone editing the preferences file by hand and writing ``WARN`` should get a
|
|
slightly quieter log, not a program that refuses to launch.
|
|
"""
|
|
assert level_from_name("WARN") == logging.WARNING
|
|
assert level_from_name("") == logging.WARNING
|