Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ logs

email-template.md

venv
venv

__pycache__
106 changes: 106 additions & 0 deletions python-api/README.md
Original file line number Diff line number Diff line change
@@ -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 …`)
1 change: 1 addition & 0 deletions python-api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
flask==3.0.0
pytest==9.0.3
22 changes: 22 additions & 0 deletions python-api/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
104 changes: 104 additions & 0 deletions python-api/tests/test_parse_logs.py
Original file line number Diff line number Diff line change
@@ -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
Loading