style(project): 🎨 improve all docstrings and centralize mail variables in mail_utils
This commit is contained in:
@@ -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
@@ -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])
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user