Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one. 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
|