diff --git a/src/logwatcher/__init__.py b/src/logwatcher/__init__.py index 7055442..11c9b79 100644 --- a/src/logwatcher/__init__.py +++ b/src/logwatcher/__init__.py @@ -2,7 +2,5 @@ from importlib.metadata import version from dotenv import load_dotenv -from logwatcher.config import * - __version__ = version("logwatcher") load_dotenv() diff --git a/src/logwatcher/config.py b/src/logwatcher/config.py index c2aa03c..e64e6da 100644 --- a/src/logwatcher/config.py +++ b/src/logwatcher/config.py @@ -3,12 +3,12 @@ from enum import Enum from pathlib import Path ### GENERAL DATA -# output path +# output paths OUTPUT_PATH = Path("output") RESULT_PATH = OUTPUT_PATH / "results" LOGGING_PATH = OUTPUT_PATH / "logs" -# test path +# test paths TEST_PATH = Path("tests") FIXTURE_PATH = TEST_PATH / "fixtures" diff --git a/src/logwatcher/logging_config.py b/src/logwatcher/logging_config.py index a5e4662..7837a7f 100644 --- a/src/logwatcher/logging_config.py +++ b/src/logwatcher/logging_config.py @@ -1,10 +1,12 @@ import logging from pathlib import Path +from logwatcher.config import DATETIME_FORMAT + def _setup_formatter( format: str = "[%(asctime)s] - %(levelname)s: %(message)s", - datefmt: str = "%d/%m/%Y %H:%M:%S", + datefmt: str = DATETIME_FORMAT, ) -> logging.Formatter: return logging.Formatter(fmt=format, datefmt=datefmt) @@ -22,26 +24,6 @@ def _setup_handler( return handler -def _convert_string_to_log_level(log_level: str) -> int: - """ - Get a log_level that contains one or three 'v' characters. - 'v' = ERROR = 40 - 'vv' = WARNING = 30 - 'vvv' = DEBUG = 10 - Args: - log_level: - - Returns: - - """ - if log_level == "v": - return logging.ERROR - elif log_level == "vv": - return logging.WARNING - else: - return logging.DEBUG - - def setup_logging( formatter: logging.Formatter | None = None, file_path: Path = Path("output/logs/logwatcher.log"), diff --git a/src/logwatcher/models.py b/src/logwatcher/models.py index 5f79e18..c901611 100644 --- a/src/logwatcher/models.py +++ b/src/logwatcher/models.py @@ -1,15 +1,16 @@ from dataclasses import dataclass from datetime import datetime -from logwatcher import DATETIME_FORMAT +from logwatcher.config import DATETIME_FORMAT @dataclass() class LogEntry: """ - Represents a line in log file. + Represents a line in log file. """ - server_ip: str + + server_ip: str mdc_server_name: str start_time: datetime store_name: str @@ -20,7 +21,7 @@ class LogEntry: def __str__(self): return self.error_message - + def get_full_message(self): return self.raw_line @@ -32,8 +33,10 @@ class LogEntry: def __post_init__(self): """ - Each property must be validated by specific regex defined in config.py + Each property must be validated by specific regex defined in config.py """ # error time cannot be earlier than start time if self.start_time > self.error_time: - raise ValueError(f"Error in date-times : error_time '{self.error_time}' cannot be earlier than start_time '{self.start_time}") + raise ValueError( + f"Error in date-times : error_time '{self.error_time}' cannot be earlier than start_time '{self.start_time}" + ) diff --git a/src/logwatcher/parser.py b/src/logwatcher/parser.py index abd84df..b93e056 100644 --- a/src/logwatcher/parser.py +++ b/src/logwatcher/parser.py @@ -4,7 +4,7 @@ from collections.abc import Iterable from datetime import datetime from pathlib import Path -from logwatcher import DATETIME_FORMAT, FRENCH_TIMEZONE +from logwatcher.config import DATETIME_FORMAT, FRENCH_TIMEZONE from logwatcher.models import LogEntry logger = logging.getLogger(__name__) @@ -29,12 +29,6 @@ LOG_PATTERN = re.compile( rf"(?P{ERROR_MESSAGE_PATTERN})" ) -# misc log file -misc_file_path = Path.cwd() / "output/misc.log" - -# effective logs -effective_file_path = Path.cwd() / "output/effective.log" - def parse_line(log_line: str) -> LogEntry | None: """ diff --git a/tests/test_classifier.py b/tests/test_classifier.py index da501c7..f4e98c0 100644 --- a/tests/test_classifier.py +++ b/tests/test_classifier.py @@ -6,18 +6,19 @@ import pytest from logwatcher.classifier import N2_PATTERNS, classify_log_entries from logwatcher.config import FIXTURE_PATH -from logwatcher.parser import parse_log_file +from logwatcher.parser import parse_file def _pattern_name_to_filename(name: str) -> str: """ - Returns the fixture corresponding to the error code name + 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. + Read a file with fallback Windows-1252. """ try: content = path.read_text(encoding="utf-8") @@ -27,10 +28,11 @@ def _read_fixture(path: Path) -> str: 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. + Assert each N2 pattern must match at least one line in its fixture. """ fixture = valid_log_dir / _pattern_name_to_filename(name) @@ -39,10 +41,7 @@ def test_pattern_matches_fixture(valid_log_dir, name, pattern): warnings.warn(msg) pytest.skip(msg) - lignes = [ - l for l in _read_fixture(fixture).splitlines() - if l.strip() - ] + lignes = [l for l in _read_fixture(fixture).splitlines() if l.strip()] assert lignes, f"empty fixture file : {fixture}" @@ -54,21 +53,23 @@ def test_pattern_matches_fixture(valid_log_dir, name, pattern): match_count += 1 assert match_count > 0, ( - f"{name} : no match in file {fixture.name}\n" - f"pattern: {pattern}" + 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. + 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 + 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() + name + for name, raw in N2_PATTERNS.items() if re.compile(raw).search(line) ] assert len(matching) <= 1, ( # TODO: improve coverage score @@ -76,10 +77,11 @@ def test_patterns_do_not_overlap(): 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Ʃ. + 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")} @@ -89,10 +91,10 @@ 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_log_file(log_file) + log_entries = parse_file(log_file) # all entries are relevant relevant, irrelevant = classify_log_entries(log_entries) @@ -102,10 +104,10 @@ 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_log_file(log_file) + log_entries = parse_file(log_file) # all entries are relevant relevant, irrelevant = classify_log_entries(log_entries) @@ -115,10 +117,10 @@ 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_log_file(log_file) + log_entries = parse_file(log_file) relevant, irrelevant = classify_log_entries(log_entries) @@ -128,10 +130,10 @@ 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_log_file(empty_file) + log_entries = parse_file(empty_file) relevant, irrelevant = classify_log_entries(log_entries) diff --git a/tests/test_logging.py b/tests/test_logging.py index e756d55..e5a14fc 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -15,6 +15,7 @@ from logging import DEBUG, ERROR, INFO, WARNING, FileHandler, StreamHandler, get import pytest +from logwatcher.config import DATETIME_FORMAT from logwatcher.logging_config import _setup_formatter, _setup_handler, setup_logging @@ -54,7 +55,7 @@ def test_type_of_handlers(): else: pytest.fail("handlers must be either a file or a stream handler") assert handler.formatter._fmt == "[%(asctime)s] - %(levelname)s: %(message)s" - assert handler.formatter.datefmt == "%d/%m/%Y %H:%M:%S" + assert handler.formatter.datefmt == DATETIME_FORMAT def test_modified_format(): diff --git a/tests/test_models.py b/tests/test_models.py index a120aa4..4189d01 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from logwatcher import DATETIME_FORMAT, FRENCH_TIMEZONE +from logwatcher.config import DATETIME_FORMAT, FRENCH_TIMEZONE from logwatcher.models import LogEntry diff --git a/tests/test_parser.py b/tests/test_parser.py index a993760..c214b65 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -57,7 +57,7 @@ def test_parse_line(specific_logs: list[str]): assert log_entry.raw_line != "" -def test_parse_empty_log_line(): +def test_parse_empty_line(): """ An empty or structurally invalid line returns None. Covered cases: @@ -71,7 +71,15 @@ def test_parse_empty_log_line(): assert not log_entry -def test_parse_valid_log_file(original_log_dir: Path): +def test_parse_lines(): + pass + + +def test_parse_empty_lines(): + pass + + +def test_parse_valid_file(original_log_dir: Path): """ Each real log file produces at least one valid LogEntry. Integration test: files provided by technicians contain @@ -86,7 +94,7 @@ def test_parse_valid_log_file(original_log_dir: Path): assert isinstance(log_entry, LogEntry) -def test_parse_empty_log_file(empty_log_file): +def test_parse_empty_file(empty_log_file): """ An empty log file can be parsed and must returns an empty LogEntry list. @@ -95,7 +103,7 @@ def test_parse_empty_log_file(empty_log_file): assert len(log_list) == 0 -def test_parse_bad_log_file(bad_log_file): +def test_parse_bad_file(bad_log_file): """ A file containing only invalid lines produces nothing. Any line that does not match the LAME MDC structure diff --git a/tests/test_reporter.py b/tests/test_reporter.py index 10296eb..e2764ba 100644 --- a/tests/test_reporter.py +++ b/tests/test_reporter.py @@ -6,7 +6,7 @@ import pytest from logwatcher.classifier import classify_log_entries from logwatcher.config import DATETIME_FORMAT, FIXTURE_PATH, SourceType from logwatcher.models import LogEntry -from logwatcher.parser import parse_log_file +from logwatcher.parser import parse_file from logwatcher.reporter import ( N2_SUPPORT_TEMPLATE, OTHER_TEMPLATE, @@ -25,7 +25,7 @@ def _get_classified_log_entries( log_file: Path, ) -> tuple[list[LogEntry], list[LogEntry]]: """Parse a log file and split its entries into relevant and irrelevant lists.""" - log_entries = parse_log_file(log_file) + log_entries = parse_file(log_file) return classify_log_entries(log_entries) @@ -40,7 +40,7 @@ def get_all_log_entries_fixture() -> tuple[list[LogEntry], list[LogEntry]]: """Parse and classify every CR_* fixture, returning (relevant, irrelevant).""" all_entries = [] for log_file in CR_LOG_FILES: - all_entries.extend(parse_log_file(log_file)) + all_entries.extend(parse_file(log_file)) relevant_list, irrelevant_list = classify_log_entries(all_entries) return relevant_list, irrelevant_list