Each test docstring states the behaviour that the test proves, in the simple present. No assertion message, test id, or other string literal changed. Four statements now agree with the code: the Trace highlight stays on the last row, a menu count, and two others. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
111 lines
4.3 KiB
Python
111 lines
4.3 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Tests for the log file: plain text, with a size limit, and with the things that went wrong.
|
|
|
|
The one real job of the log is to be opened by a person who has a problem. This person is
|
|
often the one who reports the problem. Each assertion here is about that user.
|
|
"""
|
|
|
|
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:
|
|
"""Each record is one readable line: 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 and a time. "It happened at 14:40" is not useful one week later.
|
|
assert line.startswith("20")
|
|
|
|
|
|
def test_the_file_is_named_plainly(tmp_path: Path) -> None:
|
|
"""The name is ``meshterm.log``, an extension that each text editor 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 level, the file has the problems, not a narration of a run that works.
|
|
|
|
This is the reason that the default is WARNING. If the dozen useful lines are under
|
|
twenty thousand dull lines, nobody reads the log.
|
|
"""
|
|
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 for the search for a problem. It keeps what WARNING removes."""
|
|
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:
|
|
"""The traceback of an exception is written under the record that reports it.
|
|
|
|
This is the main reason to attach the file to a bug report. The terminal scrolls and
|
|
the user closes it, but this copy stays.
|
|
"""
|
|
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:
|
|
"""The file rotates. A log with no size limit 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, but 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 preferences file that the user edits by hand loses log detail, not the app.
|
|
|
|
A user who edits the preferences file by hand and writes ``WARN`` gets a log that is a
|
|
little quieter. The app must not refuse to start.
|
|
"""
|
|
assert level_from_name("WARN") == logging.WARNING
|
|
assert level_from_name("") == logging.WARNING
|