From ea685fc68fafbd25fc0b897f85869697e7873148 Mon Sep 17 00:00:00 2001 From: NathanTesseyre Date: Fri, 22 May 2026 12:56:48 +0200 Subject: [PATCH 1/2] feat: add unit tests to python-api --- .gitignore | 4 +- python-api/README.md | 106 ++++++++++++++++++++++++++++ python-api/requirements.txt | 1 + python-api/tests/conftest.py | 22 ++++++ python-api/tests/test_parse_logs.py | 104 +++++++++++++++++++++++++++ 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 python-api/README.md create mode 100644 python-api/tests/conftest.py create mode 100644 python-api/tests/test_parse_logs.py diff --git a/.gitignore b/.gitignore index b8a7072..69c3264 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,6 @@ logs email-template.md -venv \ No newline at end of file +venv + +__pycache__ \ No newline at end of file diff --git a/python-api/README.md b/python-api/README.md new file mode 100644 index 0000000..6f48f9b --- /dev/null +++ b/python-api/README.md @@ -0,0 +1,106 @@ +# python-api — README Tests + +API Flask d'analyse de logs Azure. Ce document explique comment lancer la suite de tests unitaires et ce qu'elle couvre. + +--- + +## Prérequis + +- Python 3.8+ +- `pip` + +--- + +## Installation + +```bash +# Depuis le dossier python-api/ +pip install flask pytest +``` + +--- + +## Lancer les tests + +```bash +# Depuis le dossier python-api/ +python -m pytest tests/ -v +``` + +L'option `-v` affiche le nom de chaque test et son résultat. Sans elle, pytest affiche uniquement un résumé. + +Résultat attendu : + +``` +tests/test_parse_logs.py::test_compte_les_errors PASSED +tests/test_parse_logs.py::test_compte_les_warnings PASSED +tests/test_parse_logs.py::test_compte_les_infos PASSED +tests/test_parse_logs.py::test_errors_contient_les_lignes PASSED +tests/test_parse_logs.py::test_warnings_contient_les_lignes PASSED +tests/test_parse_logs.py::test_infos_non_incluses_dans_la_reponse PASSED +tests/test_parse_logs.py::test_fichier_vide PASSED +tests/test_parse_logs.py::test_ignore_les_lignes_vides PASSED +tests/test_parse_logs.py::test_lignes_sans_niveau_connue_ignorees PASSED +tests/test_parse_logs.py::test_mix_de_niveaux PASSED + +10 passed in 0.02s +``` + +--- + +## Structure des fichiers de test + +``` +python-api/ +├── app.py +├── requirements.txt +└── tests/ + ├── __init__.py + ├── conftest.py ← mock du config.json au chargement du module + └── test_parse_logs.py ← 10 tests unitaires sur parse_logs() +``` + +### Rôle de `conftest.py` + +`app.py` ouvre `config.json` dès son import (au niveau module). `conftest.py` intercepte cet appel avant que pytest ne charge les tests, en substituant un faux fichier de configuration en mémoire. Sans lui, pytest échoue au démarrage avec un `FileNotFoundError`. + +--- + +## Ce que les tests couvrent + +Les tests ciblent la fonction `parse_logs(filepath)` définie dans `app.py`. Elle lit un fichier de logs ligne par ligne et retourne un dictionnaire comptant et listant les événements par niveau. + +### Comptage par niveau (3 tests) + +| Test | Ce qu'il vérifie | +|---|---| +| `test_compte_les_errors` | `error_count` est égal au nombre de lignes contenant `ERROR` | +| `test_compte_les_warnings` | `warning_count` est égal au nombre de lignes contenant `WARNING` | +| `test_compte_les_infos` | `info_count` est égal au nombre de lignes contenant `INFO` | + +Chaque test passe un fichier temporaire avec uniquement le niveau concerné pour isoler le comportement. + +### Contenu des listes retournées (3 tests) + +| Test | Ce qu'il vérifie | +|---|---| +| `test_errors_contient_les_lignes` | La liste `errors` contient bien la ligne brute du fichier | +| `test_warnings_contient_les_lignes` | La liste `warnings` contient bien la ligne brute du fichier | +| `test_infos_non_incluses_dans_la_reponse` | La clé `infos` n'existe pas dans le dictionnaire retourné (les infos sont comptées mais non exposées) | + +### Cas limites (4 tests) + +| Test | Ce qu'il vérifie | +|---|---| +| `test_fichier_vide` | Un fichier vide retourne tous les compteurs à 0 et les listes vides | +| `test_ignore_les_lignes_vides` | Les lignes vides dans le fichier ne faussent pas les compteurs | +| `test_lignes_sans_niveau_connue_ignorees` | Une ligne avec un niveau inconnu (`DEBUG`, `TRACE`…) n'est comptabilisée nulle part | +| `test_mix_de_niveaux` | Un fichier mixte `ERROR` + `WARNING` + `INFO` est correctement ventilé dans les trois compteurs | + +--- + +## Ce qui n'est pas encore testé + +- La route HTTP `/api/logs` (test d'intégration Flask avec `app.test_client()`) +- Le comportement si le fichier de logs est absent ou illisible +- Les lignes contenant plusieurs niveaux dans la même ligne (ex. `ERROR WARNING …`) diff --git a/python-api/requirements.txt b/python-api/requirements.txt index 5bd19d3..e6a205d 100644 --- a/python-api/requirements.txt +++ b/python-api/requirements.txt @@ -1 +1,2 @@ flask==3.0.0 +pytest==9.0.3 \ No newline at end of file diff --git a/python-api/tests/conftest.py b/python-api/tests/conftest.py new file mode 100644 index 0000000..1fb20b6 --- /dev/null +++ b/python-api/tests/conftest.py @@ -0,0 +1,22 @@ +import sys +import types +from unittest.mock import patch, mock_open + +# Intercepter l'open() de config.json avant l'import de app +config_json = '{"api": {"host": "localhost", "port": 5000, "route": "/api/logs", "log_file": "server.log"}}' + +import builtins +_real_open = builtins.open + +def _patched_open(path, *args, **kwargs): + if str(path).endswith('config.json'): + from io import StringIO + import json + return mock_open(read_data=config_json)() + return _real_open(path, *args, **kwargs) + +builtins.open = _patched_open + +import app # noqa: E402 — import après patch + +builtins.open = _real_open # restaurer pour les tests diff --git a/python-api/tests/test_parse_logs.py b/python-api/tests/test_parse_logs.py new file mode 100644 index 0000000..db252e2 --- /dev/null +++ b/python-api/tests/test_parse_logs.py @@ -0,0 +1,104 @@ +import pytest +import os +import tempfile +from app import parse_logs + +# ------------------------------------------------------- +# Fixtures +# ------------------------------------------------------- + +@pytest.fixture +def log_file(tmp_path): + """Crée un fichier de log temporaire avec contenu contrôlé.""" + def _make(content): + f = tmp_path / "test.log" + f.write_text(content) + return str(f) + return _make + + +# ------------------------------------------------------- +# Tests — comptage par niveau +# ------------------------------------------------------- + +def test_compte_les_errors(log_file): + f = log_file( + "2024-01-15 08:02:45 ERROR Failed to connect\n" + "2024-01-15 08:05:33 ERROR Auth failed\n" + ) + result = parse_logs(f) + assert result["error_count"] == 2 + +def test_compte_les_warnings(log_file): + f = log_file( + "2024-01-15 08:01:22 WARNING High memory: 78%\n" + "2024-01-15 08:06:15 WARNING CPU spike: 92%\n" + "2024-01-15 08:16:30 WARNING SSL expires soon\n" + ) + result = parse_logs(f) + assert result["warning_count"] == 3 + +def test_compte_les_infos(log_file): + f = log_file( + "2024-01-15 08:00:01 INFO Application started\n" + "2024-01-15 08:03:10 INFO Request processed\n" + ) + result = parse_logs(f) + assert result["info_count"] == 2 + + +# ------------------------------------------------------- +# Tests — contenu des listes +# ------------------------------------------------------- + +def test_errors_contient_les_lignes(log_file): + ligne = "2024-01-15 08:02:45 ERROR Failed to connect" + f = log_file(ligne + "\n") + result = parse_logs(f) + assert ligne in result["errors"] + +def test_warnings_contient_les_lignes(log_file): + ligne = "2024-01-15 08:01:22 WARNING High memory: 78%" + f = log_file(ligne + "\n") + result = parse_logs(f) + assert ligne in result["warnings"] + +def test_infos_non_incluses_dans_la_reponse(log_file): + """Les infos sont comptées mais pas exposées dans la liste.""" + f = log_file("2024-01-15 08:00:01 INFO Application started\n") + result = parse_logs(f) + assert "infos" not in result + + +# ------------------------------------------------------- +# Tests — cas limites +# ------------------------------------------------------- + +def test_fichier_vide(log_file): + f = log_file("") + result = parse_logs(f) + assert result == {"error_count": 0, "warning_count": 0, "info_count": 0, + "errors": [], "warnings": []} + +def test_ignore_les_lignes_vides(log_file): + f = log_file("\n\n2024-01-15 08:00:01 INFO App started\n\n") + result = parse_logs(f) + assert result["info_count"] == 1 + +def test_lignes_sans_niveau_connue_ignorees(log_file): + f = log_file("2024-01-15 08:00:01 DEBUG Something happened\n") + result = parse_logs(f) + assert result["error_count"] == 0 + assert result["warning_count"] == 0 + assert result["info_count"] == 0 + +def test_mix_de_niveaux(log_file): + f = log_file( + "2024-01-15 08:00:01 INFO Start\n" + "2024-01-15 08:01:22 WARNING Memory high\n" + "2024-01-15 08:02:45 ERROR Crash\n" + ) + result = parse_logs(f) + assert result["error_count"] == 1 + assert result["warning_count"] == 1 + assert result["info_count"] == 1 From a0bd2394db151e75a7ebb85e5c8459f13a3408e1 Mon Sep 17 00:00:00 2001 From: NathanTesseyre Date: Fri, 22 May 2026 12:59:10 +0200 Subject: [PATCH 2/2] feat: add unit tests to ci --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8127191..a5e11e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,5 +14,15 @@ jobs: - name: Checkout du code uses: actions/checkout@v4 - - name: Exemple d'étape - run: echo "Ajoute tes étapes de build/test ici !" + - name: Installation de Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Installation des dépendances + run: pip install -r requirements.txt + working-directory: python-api + + - name: Lancement des tests + run: python -m pytest tests/ -v + working-directory: python-api