From 63893513eec16628525cddebc48f9926a9ba4373 Mon Sep 17 00:00:00 2001 From: maurane glaude Date: Wed, 29 Jul 2026 19:42:52 +0200 Subject: [PATCH] test(logging); add tests for setup_handler and setup_formatter function --- .gitignore | 1 + .vscode/settings.json | 7 +++ pyproject.toml | 1 + src/logwatcher/logging.py | 23 ++++----- tests/conftest.py | 19 +++++++ tests/test_logging.py | 101 ++++++++++++++++++++++++++++++++++++++ uv.lock | 77 +++++++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tests/conftest.py create mode 100644 tests/test_logging.py diff --git a/.gitignore b/.gitignore index 147b8c2..7a65ece 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ output/ src/logwatcher/__pycache__/ *.log +__pycache__ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index df79276..4577fac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "mypy>=2.3.0", + "pytest>=9.1.1", ] [project.scripts] diff --git a/src/logwatcher/logging.py b/src/logwatcher/logging.py index 47050c5..501acff 100644 --- a/src/logwatcher/logging.py +++ b/src/logwatcher/logging.py @@ -2,14 +2,14 @@ import logging from pathlib import Path -def setup_formatter( +def _setup_formatter( format: str = '[%(asctime)s] - %(levelname)s: %(message)s', datefmt: str = '%d/%m/%Y %H:%M:%S' ) -> logging.Formatter: return logging.Formatter(fmt=format, datefmt=datefmt) -def setup_file_handler(formatter: logging.Formatter, level: int , filename: Path | None = None) -> logging.Handler: +def _setup_handler(formatter: logging.Formatter, level: int , filename: Path | None = None) -> logging.Handler: handler: logging.Handler if filename: handler = logging.FileHandler(filename=filename, encoding='utf-8') @@ -20,23 +20,24 @@ def setup_file_handler(formatter: logging.Formatter, level: int , filename: Path return handler -def setup_logging() -> logging.Logger: +def setup_logging(formatter: logging.Formatter|None = None, file_path: Path = Path('output/logs/logwatcher.log')) -> logging.Logger: """ Create a new instance of Logger named 'logger' customized for the logwatcher package. The format display in order the time, the level of log and the log message The logs are only redirect in the standard output. """ - logger = logging.getLogger("logwatcher_log") + _formatter = formatter if formatter else _setup_formatter() - formatter = setup_formatter() + standard_output = _setup_handler(_formatter, logging.ERROR) + file_output = _setup_handler(_formatter, logging.DEBUG, file_path) - standard_output = setup_file_handler(formatter, logging.ERROR) + root = logging.getLogger() + root.setLevel(logging.DEBUG) - file_path = Path('output/logs/logwatcher.log') - file_output = setup_file_handler(formatter, logging.DEBUG, file_path) + root.addHandler(standard_output) + root.addHandler(file_output) - - logger.addHandler(standard_output) - logger.addHandler(file_output) + logger = logging.getLogger("logwatcher") + logger.setLevel(logging.NOTSET) return logger diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8534755 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +# tests/conftest.py +import os +from pathlib import Path + +import pytest + +from logwatcher.logging import setup_logging + +# Forcer un dossier temporaire pour les tests +os.environ["PYTEST_DEBUG_TEMPROOT"] = str(Path("output/test").absolute()) + +@pytest.fixture +def tmp_log_file(tmp_path): + return tmp_path / "logwatcher_test.log" + +@pytest.fixture(autouse=True) +def tmp_setup_logging(tmp_log_file): + """Redirige les logs vers un dossier temporaire pendant les tests.""" + setup_logging(file_path=tmp_log_file) \ No newline at end of file diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..e6e15f6 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,101 @@ +""" +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 re +from logging import DEBUG, ERROR, INFO, WARNING, FileHandler, StreamHandler, getLogger + +import pytest + +from logwatcher import logger +from logwatcher.logging import _setup_formatter, _setup_handler, setup_logging + + +def test_setup_handler(tmp_log_file): + formatter = _setup_formatter() + handler = _setup_handler(formatter=formatter, level=DEBUG) + assert isinstance(handler, StreamHandler) + + handler = _setup_handler(formatter=formatter, level=INFO,filename=tmp_log_file) + assert isinstance(handler, FileHandler) + +def test_count_of_handler(tmp_log_file): + logger = getLogger() + count_file_handler = 0 + count_stream_handler = 0 + + for handler in logger.handlers: + if isinstance(handler, FileHandler): + count_file_handler += 1 + elif isinstance(handler, StreamHandler): + count_stream_handler += 1 + else: + pass + + assert count_file_handler >= 1 + assert count_stream_handler >= 1 + +def test_type_of_handlers(): + for handler in logger.handlers: + if isinstance(handler, FileHandler): + assert handler.level == DEBUG + elif isinstance(handler, StreamHandler): + assert handler.level == ERROR + 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' + +def test_modified_format(): + """ + when starting, the logger is first intialized, thus two handlers are at position + 1 and 2 in logger.handlers list. + """ + logger = setup_logging() + for handler in logger.handlers[2:]: + assert handler.formatter._fmt == '[%(asctime)s] - %(levelname)s: %(message)s' + + formatter_different_format = _setup_formatter(format='%(asctime)s - %(levelname)s: %(message)s') + logger = setup_logging(formatter=formatter_different_format) + for handler in logger.handlers[4:]: + assert handler.formatter._fmt == '%(asctime)s - %(levelname)s: %(message)s' + + formatter_different_datefmt = _setup_formatter(datefmt='%a %b %Y at %Hh%M') + logger = setup_logging(formatter=formatter_different_datefmt) + for handler in logger.handlers[6:]: + assert handler.formatter._fmt == '[%(asctime)s] - %(levelname)s: %(message)s' + assert handler.formatter.datefmt == '%a %b %Y at %Hh%M' + + +def test_logging_in_correct_path(tmp_log_file): + # must be both in file and stdout + logger.critical("hi- BYE") + logger.error("hi... ok, bye") + logger.warning("hi, are you alright?") + + # only in file + logger.info("hi there") + logger.debug("hi, how are you? it's been a while!") + + with open(tmp_log_file) as log_file: + content = log_file.readlines() + regex = r'\[\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}\] - ' + + assert len(content) == 5 + for line in content: + assert re.search(regex, line) + + # test critical to warning + 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]) + assert re.search(r'INFO: hi there', content[3]) + assert logger.getEffectiveLevel() < WARNING + assert re.search(r"DEBUG: hi, how are you\? it's been a while!", content[4]) + \ No newline at end of file diff --git a/uv.lock b/uv.lock index 7986e15..37860b8 100644 --- a/uv.lock +++ b/uv.lock @@ -47,6 +47,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "librt" version = "0.13.0" @@ -140,6 +170,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "mypy" }, + { name = "pytest" }, ] [package.optional-dependencies] @@ -152,6 +183,7 @@ dev = [ requires-dist = [ { name = "mypy", specifier = ">=2.3.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.3.0" }, + { name = "pytest", specifier = ">=9.1.1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.16.0" }, ] provides-extras = ["dev"] @@ -225,6 +257,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -234,6 +275,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "ruff" version = "0.16.0"