import re import warnings from pathlib import Path import pytest from logwatcher.classifier import N2_PATTERNS, classify_log_entries from logwatcher.config import FIXTURE_PATH from logwatcher.parser import parse_file def _pattern_name_to_filename(name: str) -> str: """ Returns 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. """ try: content = path.read_text(encoding="utf-8") except UnicodeDecodeError: content = path.read_text(encoding="windows-1252") if "\ufffd" not in content: return content return path.read_text(encoding="windows-1252") # TODO: improve coverage score @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. """ fixture = valid_log_dir / _pattern_name_to_filename(name) if not fixture.exists(): msg = f"No fixture named '{fixture}'" warnings.warn(msg) pytest.skip(msg) lignes = [l for l in _read_fixture(fixture).splitlines() if l.strip()] assert lignes, f"empty fixture file : {fixture}" match_count = 0 for ligne in lignes: match = re.search(pattern, ligne) if match: match_count += 1 assert match_count > 0, ( f"{name} : no match in file {fixture.name}\npattern: {pattern}" ) def test_patterns_do_not_overlap(): """ 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 = [ line for line in _read_fixture(fixture).splitlines() if line.strip() ] # TODO: improve coverage score for line in line_list: # TODO: improve coverage score matching = [ # TODO: improve coverage score name for name, raw in N2_PATTERNS.items() if re.compile(raw).search(line) ] assert len(matching) <= 1, ( # TODO: improve coverage score f"Chevauchement détecté sur {fixture.name} : " f"{matching} match line : {line[:80]}" ) 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é. """ 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 assert not orphan, f"Fixtures orphelines (sans pattern) : {sorted(orphan)}" def test_classify_log_entries_all_relevant(valid_log_dir: Path): """ Must return an empty irrelevant log entry list """ log_file = valid_log_dir / "only_relevant_logs.txt" log_entries = parse_file(log_file) # all entries are relevant relevant, irrelevant = classify_log_entries(log_entries) assert relevant and not irrelevant def test_classify_log_entries_none_relevant(valid_log_dir: Path): """ Must return an empty relevant log entry list """ log_file = valid_log_dir / "no_relevant_logs.txt" log_entries = parse_file(log_file) # all entries are relevant relevant, irrelevant = classify_log_entries(log_entries) assert not relevant and irrelevant def test_classify_log_entries_mixed(valid_log_dir: Path): """ Test classification on relevant and irrelevant log entry list """ log_file = valid_log_dir / "mixed_logs.txt" log_entries = parse_file(log_file) relevant, irrelevant = classify_log_entries(log_entries) assert len(relevant) and len(irrelevant) assert len(relevant) + len(irrelevant) == len(log_entries) def test_classify_log_entries_empty_log_entries(invalid_log_dir: Path): """ Test classification on an empty log entry list """ empty_file = invalid_log_dir / "empty_file.txt" log_entries = parse_file(empty_file) relevant, irrelevant = classify_log_entries(log_entries) assert not relevant and not irrelevant