style(project): 🎨 improve all docstrings and centralize mail variables in mail_utils

This commit is contained in:
2026-09-17 12:59:07 +02:00
parent 6909ee7acf
commit 5e43a0cbb8
17 changed files with 116 additions and 101 deletions
+9 -29
View File
@@ -10,16 +10,12 @@ from logwatcher.parser import parse_file
def _pattern_name_to_filename(name: str) -> str:
"""
Returns the fixture corresponding to the error code name
"""
"""Return the fixture corresponding to the error code name."""
return f"CR_{name.lower()}.txt"
def _read_fixture(path: Path) -> str:
"""
Read a file with fallback Windows-1252.
"""
"""Read a file with fallback Windows-1252."""
try:
content = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
@@ -31,10 +27,7 @@ def _read_fixture(path: Path) -> str:
@pytest.mark.parametrize("name, pattern", N2_PATTERNS.items())
def test_pattern_matches_fixture(valid_log_dir, name, pattern):
"""
Assert each N2 pattern must match at least one line in its fixture.
"""
"""Assert each N2 pattern must match at least one line in its fixture."""
fixture = valid_log_dir / _pattern_name_to_filename(name)
if not fixture.exists():
msg = f"No fixture named '{fixture}'"
@@ -58,9 +51,7 @@ def test_pattern_matches_fixture(valid_log_dir, name, pattern):
def test_patterns_do_not_overlap():
"""
Assert that a log line can match only one N2 pattern.
"""
"""Assert that a log line can match only one N2 pattern."""
fixtures_dir = FIXTURE_PATH
for fixture in fixtures_dir.glob("CR_*.txt"):
line_list = [
@@ -79,10 +70,7 @@ def test_patterns_do_not_overlap():
def test_no_orphan_fixtures():
"""
Assert that a fixture exist only if its associated pattern exists.
Aucun fichier de fixture ne doit exister sans pattern associé.
"""
"""Assert that a fixture exist only if its associated pattern exists."""
expected = {f"CR_{name.lower()}.txt" for name in N2_PATTERNS}
actual = {f.name for f in FIXTURE_PATH.glob("CR_*.txt")}
orphan = actual - expected
@@ -90,9 +78,7 @@ def test_no_orphan_fixtures():
def test_classify_log_entries_all_relevant(valid_log_dir: Path):
"""
Must return an empty irrelevant log entry list
"""
"""Must return an empty irrelevant log entry list."""
log_file = valid_log_dir / "only_relevant_logs.txt"
log_entries = parse_file(log_file)
@@ -103,9 +89,7 @@ def test_classify_log_entries_all_relevant(valid_log_dir: Path):
def test_classify_log_entries_none_relevant(valid_log_dir: Path):
"""
Must return an empty relevant log entry list
"""
"""Must return an empty relevant log entry list."""
log_file = valid_log_dir / "no_relevant_logs.txt"
log_entries = parse_file(log_file)
@@ -116,9 +100,7 @@ def test_classify_log_entries_none_relevant(valid_log_dir: Path):
def test_classify_log_entries_mixed(valid_log_dir: Path):
"""
Test classification on relevant and irrelevant log entry list
"""
"""Test classification on relevant and irrelevant log entry list."""
log_file = valid_log_dir / "mixed_logs.txt"
log_entries = parse_file(log_file)
@@ -129,9 +111,7 @@ def test_classify_log_entries_mixed(valid_log_dir: Path):
def test_classify_log_entries_empty_log_entries(invalid_log_dir: Path):
"""
Test classification on an empty log entry list
"""
"""Test classification on an empty log entry list."""
empty_file = invalid_log_dir / "empty_file.txt"
log_entries = parse_file(empty_file)
+17 -14
View File
@@ -1,14 +1,3 @@
"""
this module contains all tests logging-related :
- the logger should have at least one StreamHandler and one FileHandler each time
logwatcher is launched.
- the StreamHandler must watch at an error level
- the FileHandler must watch at a debug
level
-
"""
import logging
import re
from logging import DEBUG, ERROR, INFO, WARNING, FileHandler, StreamHandler, getLogger
@@ -20,6 +9,7 @@ from logwatcher.logging_config import _setup_formatter, _setup_handler, setup_lo
def test_setup_handler(tmp_log_file):
"""Assert `setup_handler` instanciate one StreamHandler and one FileHandler."""
formatter = _setup_formatter()
handler = _setup_handler(formatter=formatter, level=DEBUG)
assert isinstance(handler, StreamHandler)
@@ -29,6 +19,10 @@ def test_setup_handler(tmp_log_file):
def test_count_of_handler(tmp_log_file):
"""Assert `setup_handler` instanciate exactly one StreamHandler and one FileHandler.
Must have exactly two handlers in total.
"""
logger = getLogger()
count_file_handler = 0
count_stream_handler = 0
@@ -46,6 +40,10 @@ def test_count_of_handler(tmp_log_file):
def test_type_of_handlers():
"""Assert each handlers capture the right log level and use the right log format.
FileHandler must capute DEBUG logs while StreamHandler only capture ERROR logs.
"""
logger = logging.getLogger("logwatcher")
for handler in logger.handlers:
if isinstance(handler, FileHandler):
@@ -59,8 +57,9 @@ def test_type_of_handlers():
def test_modified_format():
"""
when starting, the logger is first intialized, thus two handlers are at position
"""Assert `setup_logging` allow to use another log format.
When starting, the logger is first intialized, thus two handlers are at position
1 and 2 in logger.handlers list.
"""
logger = setup_logging()
@@ -82,6 +81,10 @@ def test_modified_format():
def test_logging_in_correct_path(tmp_log_file):
"""Assert log file is correctly written in output directory.
Check content of the file. It must contain logs from CRITICAL to DEBUG level.
"""
logger = logging.getLogger("logwatcher")
# must be both in file and stdout
logger.critical("hi- BYE")
@@ -100,7 +103,7 @@ def test_logging_in_correct_path(tmp_log_file):
for line in content:
assert re.search(regex, line)
# test critical to warning
# test from critical to debug logs
assert re.search(r"CRITICAL: hi- BYE", content[0])
assert re.search(r"ERROR: hi... ok, bye", content[1])
assert re.search(r"WARNING: hi, are you alright?", content[2])
+3 -3
View File
@@ -110,7 +110,7 @@ class OkProtocol:
class OkAccount:
"""Account with a protocol that returns a version successfully."""
def __init__(self): # noqa: D102
def __init__(self): # noqa: D107
self.protocol = OkProtocol()
@@ -380,7 +380,7 @@ def test_fetch_log_messages_folder_path(make_mock_account):
# extract_log_lines(content) #
##############################
def test_extract_log_lines_split_and_clean(original_log_dir: Path):
"""Splits content, strips \r\n, removes empty lines."""
r"""Split content, strips '\r\n', removes empty lines."""
file = original_log_dir / "CR_20260727110008.txt"
content = file.read_text(encoding="windows-1252")
lines = extract_log_lines(content)
@@ -412,7 +412,7 @@ def test_extract_log_lines_no_trailing_newline():
def test_extract_log_lines_crlf():
"""Handles Windows \r\n line endings."""
r"""Handles Windows \r\n line endings."""
content = (
"Répertoire scanné : \\192.168.60.40\\e$\\MDC_1110\\Logs\n"
"\\192.168.60.40\\e$\\MDC_1110\\Logs\\26\\07\\27\\20260727083117.txt [27/07/2026 08:31:26] DOSSIER EN COURS : FLAMMIER [27/07/2026 08:31:53] Erreur FTP commande : Requested action not taken\r\n"
@@ -1,22 +1,22 @@
from unittest.mock import MagicMock
from logwatcher.utils import get_or_create_folder
from logwatcher.mail_utils import get_or_create_folder
def test_get_or_create_analyzed_folder_exists(make_mock_account, monkeypatch):
"""
Returns the existing 'Analyzed' folder without creating it.
"""Returns the existing 'Analyzed' folder without creating it.
Args:
make_mock_account: Fake account owning the Logs folder.
make_mock_account: Fake account owning the Logs folder
monkeypatch: MonkeyPatch to generate test context
"""
existing_folder = MagicMock()
account = make_mock_account(analyzed_folder=existing_folder)
fake_folder_cls = MagicMock()
with monkeypatch.context() as m:
m.setattr("logwatcher.utils.Folder", fake_folder_cls)
m.setattr("logwatcher.mail_utils.Folder", fake_folder_cls)
result = get_or_create_folder(account, "Analyzed")
assert result is existing_folder
@@ -30,7 +30,7 @@ def test_get_or_create_analyzed_folder_creates(make_mock_account, monkeypatch):
fake_folder_instance = MagicMock()
fake_folder_cls = MagicMock(return_value=fake_folder_instance)
with monkeypatch.context() as m:
m.setattr("logwatcher.utils.Folder", fake_folder_cls)
m.setattr("logwatcher.mail_utils.Folder", fake_folder_cls)
result = get_or_create_folder(account, "Analyzed")
assert result is fake_folder_instance
+3
View File
@@ -7,6 +7,7 @@ from logwatcher.models import LogEntry
def test_valid_log_entry():
"""Assert a log entry is correctly created when all arguments are correct."""
LogEntry(
server_ip="192.168.13.27",
mdc_server_name="MDC_720",
@@ -18,6 +19,7 @@ def test_valid_log_entry():
)
def test_log_entry_invalid_dates():
"""Assert a log entry cannot be created if start time is not later than error time."""
with pytest.raises(ValueError, match="Error in date-times"):
LogEntry(
server_ip="192.168.13.27",
@@ -30,6 +32,7 @@ def test_log_entry_invalid_dates():
)
def test_equals_models():
"""Assert two models are equals if they have the same value as attributes."""
log_entry_1 = LogEntry(
server_ip="192.168.13.27",
mdc_server_name="MDC_720",
+21 -13
View File
@@ -8,8 +8,8 @@ from logwatcher.parser import parse_file, parse_line, parse_lines
@pytest.fixture(name="empty_log_file")
def empty_log_file_fixture(invalid_log_dir: Path) -> Path:
"""
Path of an empty log file.
"""Path of an empty log file.
Ensures the parser does not raise an exception
and returns an empty list.
"""
@@ -18,8 +18,8 @@ def empty_log_file_fixture(invalid_log_dir: Path) -> Path:
@pytest.fixture(name="bad_log_file")
def bad_log_file_fixture(invalid_log_dir: Path) -> Path:
"""
Path of a file containing only incorrectly formatted lines.
"""Path of a file containing only incorrectly formatted lines.
Each line fails at a different point in LOG_PATTERN
(invalid IP, missing timestamp, missing DOSSIER EN COURS, etc.).
No LogEntry should be produced.
@@ -29,8 +29,8 @@ def bad_log_file_fixture(invalid_log_dir: Path) -> Path:
@pytest.fixture(name="specific_logs")
def specific_logs_fixture(valid_log_dir: Path) -> Path:
"""
Single log lines chosen to cover specific cases.
"""Single log lines chosen to cover specific cases.
Each line is structurally valid and must be parsed
successfully. Used for unit tests of parse_line().
"""
@@ -38,8 +38,8 @@ def specific_logs_fixture(valid_log_dir: Path) -> Path:
def test_parse_line(specific_logs: Path):
"""
Every structurally valid line produces a complete LogEntry.
"""Every structurally valid line produces a complete LogEntry.
Ensures no required field is empty or None after parsing
a line conforming to the LAME MDC format.
"""
@@ -59,8 +59,8 @@ def test_parse_line(specific_logs: Path):
def test_parse_empty_line():
"""
An empty or structurally invalid line returns None.
r"""An empty or structurally invalid line returns None.
Covered cases:
- Empty string.
- Directory header (\"Répertoire scanné : \\\\...\").
@@ -73,6 +73,7 @@ def test_parse_empty_line():
def test_parse_lines():
"""Assert parse_log find the valid logs among a mixed of logs."""
# mix of valid and invalid logs. Have 4 valid logs
original_lines = [
r"Répertoire scanné : \\192.168.13.22\e\MDC_240\Logs",
@@ -92,6 +93,7 @@ def test_parse_lines():
def test_parse_valid_lines(specific_logs: Path):
"""Assert parse_lines works for all valid lines."""
# selected valid logs
with open(specific_logs, "r") as file:
log_lines = file.readlines()
@@ -102,20 +104,24 @@ def test_parse_valid_lines(specific_logs: Path):
def test_parse_empty_lines(empty_log_file: Path):
"""Assert parsing an empty line does not raise an error and return an empty log entry list."""
with open(empty_log_file, "r") as file:
log_entries = parse_lines(file.readlines())
assert len(log_entries) == 0
def test_parse_invalid_lines(bad_log_file: Path):
"""Assert parsing an invalid line does not raise an error and return an empty log entry list."""
with open(bad_log_file, "r") as file:
log_entries = parse_lines(file.readlines())
assert len(log_entries) == 0
def test_parse_files(original_log_dir: Path, valid_log_dir: Path):
"""
"""Assert parse_files works with real life logs.
Each real log file produces at least one valid LogEntry.
Integration test: files provided by technicians contain
a mix of valid lines and lines to be ignored.
Ensures the parser extracts at least one entry per file.
@@ -130,7 +136,8 @@ def test_parse_files(original_log_dir: Path, valid_log_dir: Path):
def test_parse_empty_file(empty_log_file):
"""
"""Assert parsing an empty file does not raise an error and return an empty log entrt list instead.
An empty log file can be parsed and must returns an
empty LogEntry list.
"""
@@ -139,7 +146,8 @@ def test_parse_empty_file(empty_log_file):
def test_parse_bad_file(bad_log_file):
"""
"""Assert parsing a bad file does not raise an error and return an empty log entry list instead.
A file containing only invalid lines produces nothing.
Any line that does not match the LAME MDC structure
(IP, timestamps, DOSSIER EN COURS, error message)