test(M8): tests unitaires pour AgendaComparator (17 cas)
Couvre : agendas vides, ADDED/REMOVED/MODIFIED, tolérance ±15 min (bord inclusif), normalisation NFKC des matières, matching multi-candidats par plus petit id, comparaison ordre-insensible des enseignants/salles, statut != NORMAL, REMOVED par existence (pas par sélection), ordre déterministe et idempotence. Couverture de sync/diff.py : 92%. Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
This commit is contained in:
640
tests/unit/test_diff.py
Normal file
640
tests/unit/test_diff.py
Normal file
@@ -0,0 +1,640 @@
|
||||
"""Unit tests for AgendaComparator in pronote_sync/sync/diff.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from typing import override
|
||||
|
||||
import pytest
|
||||
|
||||
from pronote_sync.models.agenda import Lesson, LessonStatus, TheoreticalLesson
|
||||
from pronote_sync.models.diff import AgendaChangeType
|
||||
from pronote_sync.sources.theoretical.provider import TheoreticalAgendaProvider
|
||||
from pronote_sync.sync.diff import AgendaComparator
|
||||
|
||||
|
||||
class _StubProvider(TheoreticalAgendaProvider):
|
||||
"""Stub implementation of TheoreticalAgendaProvider for testing."""
|
||||
|
||||
def __init__(self, lessons: list[TheoreticalLesson]) -> None:
|
||||
"""Initialize with a fixed list of theoretical lessons."""
|
||||
self._lessons = lessons
|
||||
|
||||
@override
|
||||
def get_lessons(self, target_date: date) -> list[TheoreticalLesson]:
|
||||
"""Return the stub lessons regardless of target_date."""
|
||||
return self._lessons.copy()
|
||||
|
||||
@override
|
||||
def get_lessons_for_range(self, start_date: date, end_date: date) -> list[TheoreticalLesson]:
|
||||
"""Return the stub lessons regardless of date range."""
|
||||
return self._lessons.copy()
|
||||
|
||||
|
||||
# Target date: Monday, 2025-09-15 (weekday() = 0)
|
||||
TARGET_DATE = date(2025, 9, 15)
|
||||
|
||||
|
||||
@pytest.fixture(name="empty_provider")
|
||||
def fixture_empty_provider() -> _StubProvider:
|
||||
"""Provider with no theoretical lessons."""
|
||||
return _StubProvider([])
|
||||
|
||||
|
||||
@pytest.fixture(name="comparator")
|
||||
def fixture_comparator(empty_provider: _StubProvider) -> AgendaComparator:
|
||||
"""AgendaComparator with empty provider."""
|
||||
return AgendaComparator(empty_provider)
|
||||
|
||||
|
||||
# ==================== Test Case 1: Empty agendas ====================
|
||||
|
||||
|
||||
def test_empty_agendas(comparator: AgendaComparator) -> None:
|
||||
"""No real, no theoretical → AgendaDiff with empty changes."""
|
||||
result = comparator.compare([], TARGET_DATE)
|
||||
assert result.target_date == TARGET_DATE
|
||||
assert result.changes == ()
|
||||
|
||||
|
||||
# ==================== Test Case 2: Empty theoretical, real lessons present ====================
|
||||
|
||||
|
||||
def test_empty_theoretical_real_present(comparator: AgendaComparator) -> None:
|
||||
"""Empty theoretical, real lessons present → all real → ADDED."""
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
Lesson(
|
||||
id="real_2",
|
||||
start=datetime(2025, 9, 15, 14, 0, 0),
|
||||
end=datetime(2025, 9, 15, 15, 0, 0),
|
||||
subject="Français",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 2
|
||||
assert result.changes[0].type == AgendaChangeType.ADDED
|
||||
assert result.changes[0].lesson == real_lessons[0]
|
||||
assert result.changes[0].theoretical_lesson is None
|
||||
assert result.changes[0].details == "Cours ajouté par rapport à l'agenda théorique"
|
||||
assert result.changes[1].type == AgendaChangeType.ADDED
|
||||
assert result.changes[1].lesson == real_lessons[1]
|
||||
|
||||
|
||||
# ==================== Test Case 3: Empty real, theoretical present ====================
|
||||
|
||||
|
||||
def test_empty_real_theoretical_present() -> None:
|
||||
"""Empty real, theoretical present → all theoretical → REMOVED, sorted by id."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_b",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
TheoreticalLesson(
|
||||
id="theo_a",
|
||||
day_of_week=0,
|
||||
start_time=time(14, 0),
|
||||
end_time=time(15, 0),
|
||||
subject="Français",
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare([], TARGET_DATE)
|
||||
assert len(result.changes) == 2
|
||||
assert result.changes[0].type == AgendaChangeType.REMOVED
|
||||
assert result.changes[0].theoretical_lesson == theoretical_lessons[1] # theo_a first
|
||||
assert result.changes[0].lesson is None
|
||||
assert result.changes[0].details == "Cours supprimé par rapport à l'agenda théorique"
|
||||
assert result.changes[1].type == AgendaChangeType.REMOVED
|
||||
assert result.changes[1].theoretical_lesson == theoretical_lessons[0] # theo_b second
|
||||
|
||||
|
||||
# ==================== Test Case 4: Exact match ====================
|
||||
|
||||
|
||||
def test_exact_match() -> None:
|
||||
"""Real and theoretical at same time, same subject → no changes."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert result.changes == ()
|
||||
|
||||
|
||||
# ==================== Test Case 5: Within tolerance (±14 min) ====================
|
||||
|
||||
|
||||
def test_within_tolerance_14min() -> None:
|
||||
"""Real start 14 min before theoretical → match, MODIFIED (horaires different)."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 9, 46, 0),
|
||||
end=datetime(2025, 9, 15, 10, 46, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].type == AgendaChangeType.MODIFIED
|
||||
assert result.changes[0].lesson == real_lessons[0]
|
||||
assert result.changes[0].theoretical_lesson == theoretical_lessons[0]
|
||||
assert "horaires: 10:00–11:00 → 09:46–10:46" in result.changes[0].details
|
||||
|
||||
|
||||
# ==================== Test Case 6: At tolerance boundary (exactly 15 min) ====================
|
||||
|
||||
|
||||
def test_at_tolerance_boundary_15min() -> None:
|
||||
"""Real start exactly 15 min from theoretical → match (inclusive), MODIFIED."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 9, 45, 0),
|
||||
end=datetime(2025, 9, 15, 10, 45, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].type == AgendaChangeType.MODIFIED
|
||||
assert result.changes[0].lesson == real_lessons[0]
|
||||
assert result.changes[0].theoretical_lesson == theoretical_lessons[0]
|
||||
assert "horaires: 10:00–11:00 → 09:45–10:45" in result.changes[0].details
|
||||
|
||||
|
||||
# ==================== Test Case 7: Outside tolerance (16 min) ====================
|
||||
|
||||
|
||||
def test_outside_tolerance_16min() -> None:
|
||||
"""Real start 16 min from theoretical → no match → ADDED + REMOVED."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 9, 44, 0),
|
||||
end=datetime(2025, 9, 15, 10, 44, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 2
|
||||
assert result.changes[0].type == AgendaChangeType.ADDED
|
||||
assert result.changes[0].lesson == real_lessons[0]
|
||||
assert result.changes[1].type == AgendaChangeType.REMOVED
|
||||
assert result.changes[1].theoretical_lesson == theoretical_lessons[0]
|
||||
|
||||
|
||||
# ==================== Test Case 8: Subject normalization match ====================
|
||||
|
||||
|
||||
def test_subject_normalization_match() -> None:
|
||||
"""Real '\\u212BNGSTRÖM' (angstrom sign), theoretical 'ångström' → NFKC → same form, match, no change."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="ångström",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="\u212bNGSTRÖM",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert result.changes == ()
|
||||
|
||||
|
||||
# ==================== Test Case 9: Different normalized subjects ====================
|
||||
|
||||
|
||||
def test_different_normalized_subjects() -> None:
|
||||
"""Real 'Mathématiques', theoretical 'Français' → no match → ADDED + REMOVED."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Français",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 2
|
||||
assert result.changes[0].type == AgendaChangeType.ADDED
|
||||
assert result.changes[0].lesson == real_lessons[0]
|
||||
assert result.changes[1].type == AgendaChangeType.REMOVED
|
||||
assert result.changes[1].theoretical_lesson == theoretical_lessons[0]
|
||||
|
||||
|
||||
# ==================== Test Case 10: Multi-candidate selection by id ====================
|
||||
|
||||
|
||||
def test_multi_candidate_selection_by_id() -> None:
|
||||
"""Two theoretical candidates match one real → select the smaller id (theo_a).
|
||||
|
||||
theo_a (smaller id) has teachers identical to the real lesson (no MODIFIED);
|
||||
theo_b (larger id) has different teachers and would trigger MODIFIED if selected.
|
||||
A zero-change diff therefore proves theo_a was selected.
|
||||
"""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_b",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("Mme Martin",),
|
||||
),
|
||||
TheoreticalLesson(
|
||||
id="theo_a",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("M. Dupont",),
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("M. Dupont",),
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
# theo_a (smaller id) selected with identical teachers → no MODIFIED; theo_b matched by existence → not REMOVED
|
||||
assert result.changes == ()
|
||||
|
||||
|
||||
# ==================== Test Case 11: MODIFIED — teachers differ (order-insensitive) ====================
|
||||
|
||||
|
||||
def test_teachers_differ_order_insensitive() -> None:
|
||||
"""Real teachers ('M. Dupont', 'Mme Martin'), theoretical ('Mme Martin', 'M. Dupont') → match, NOT modified (same set)."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("Mme Martin", "M. Dupont"),
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("M. Dupont", "Mme Martin"),
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert result.changes == ()
|
||||
|
||||
|
||||
# ==================== Test Case 12: MODIFIED — teachers differ (different sets) ====================
|
||||
|
||||
|
||||
def test_teachers_differ_different_sets() -> None:
|
||||
"""Real ('M. Dupont',), theoretical ('Mme Martin',) → match, MODIFIED."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("Mme Martin",),
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("M. Dupont",),
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].type == AgendaChangeType.MODIFIED
|
||||
assert "professeurs: {'Mme Martin'} → {'M. Dupont'}" in result.changes[0].details
|
||||
|
||||
|
||||
# ==================== Test Case 13: MODIFIED — rooms differ ====================
|
||||
|
||||
|
||||
def test_rooms_differ() -> None:
|
||||
"""Real ('Salle 12',), theoretical ('Salle 15',) → match, MODIFIED."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
rooms=("Salle 15",),
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
rooms=("Salle 12",),
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].type == AgendaChangeType.MODIFIED
|
||||
assert "salles: {'Salle 15'} → {'Salle 12'}" in result.changes[0].details
|
||||
|
||||
|
||||
# ==================== Test Case 14: MODIFIED — status != NORMAL ====================
|
||||
|
||||
|
||||
def test_status_not_normal() -> None:
|
||||
"""Real status=CANCELLED, otherwise identical → match, MODIFIED with statut in details."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
status=LessonStatus.CANCELLED,
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert len(result.changes) == 1
|
||||
assert result.changes[0].type == AgendaChangeType.MODIFIED
|
||||
assert "statut: cancelled" in result.changes[0].details
|
||||
|
||||
|
||||
# ==================== Test Case 15: REMOVED by existence, not selection ====================
|
||||
|
||||
|
||||
def test_removed_by_existence_not_selection() -> None:
|
||||
"""Two theoretical match one real; real selects the smaller id; the other theoretical is a candidate (exists) → NOT REMOVED."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_a",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
TheoreticalLesson(
|
||||
id="theo_b",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
# Both theoretical lessons are candidates (matched by existence), so neither is REMOVED
|
||||
assert len(result.changes) == 0
|
||||
|
||||
|
||||
# ==================== Test Case 16: Deterministic order ====================
|
||||
|
||||
|
||||
def test_deterministic_order() -> None:
|
||||
"""Multiple ADDED, MODIFIED, REMOVED in same run → verify exact order (reals in input order, then theoreticals sorted by id)."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_c",
|
||||
day_of_week=0,
|
||||
start_time=time(15, 0),
|
||||
end_time=time(16, 0),
|
||||
subject="Histoire",
|
||||
),
|
||||
TheoreticalLesson(
|
||||
id="theo_a",
|
||||
day_of_week=0,
|
||||
start_time=time(8, 0),
|
||||
end_time=time(9, 0),
|
||||
subject="Physique",
|
||||
),
|
||||
TheoreticalLesson(
|
||||
id="theo_b",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
teachers=("M. Dupont",),
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
Lesson(
|
||||
id="real_2",
|
||||
start=datetime(2025, 9, 15, 14, 0, 0),
|
||||
end=datetime(2025, 9, 15, 15, 0, 0),
|
||||
subject="Informatique",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result = comparator.compare(real_lessons, TARGET_DATE)
|
||||
|
||||
# real_1 matches theo_b but has different teachers → MODIFIED
|
||||
# real_2 has no match → ADDED
|
||||
# theo_a and theo_c are not matched by existence → REMOVED (sorted by id: theo_a, theo_c)
|
||||
assert len(result.changes) == 4
|
||||
|
||||
# First: real_1 MODIFIED
|
||||
assert result.changes[0].type == AgendaChangeType.MODIFIED
|
||||
assert result.changes[0].lesson == real_lessons[0]
|
||||
|
||||
# Second: real_2 ADDED
|
||||
assert result.changes[1].type == AgendaChangeType.ADDED
|
||||
assert result.changes[1].lesson == real_lessons[1]
|
||||
|
||||
# Third: theo_a REMOVED
|
||||
assert result.changes[2].type == AgendaChangeType.REMOVED
|
||||
assert result.changes[2].theoretical_lesson == theoretical_lessons[1] # theo_a
|
||||
|
||||
# Fourth: theo_c REMOVED
|
||||
assert result.changes[3].type == AgendaChangeType.REMOVED
|
||||
assert result.changes[3].theoretical_lesson == theoretical_lessons[0] # theo_c
|
||||
|
||||
|
||||
# ==================== Test Case 17: Idempotence ====================
|
||||
|
||||
|
||||
def test_idempotence() -> None:
|
||||
"""Call compare twice with same inputs → identical AgendaDiff."""
|
||||
theoretical_lessons = [
|
||||
TheoreticalLesson(
|
||||
id="theo_1",
|
||||
day_of_week=0,
|
||||
start_time=time(10, 0),
|
||||
end_time=time(11, 0),
|
||||
subject="Mathématiques",
|
||||
),
|
||||
]
|
||||
real_lessons = [
|
||||
Lesson(
|
||||
id="real_1",
|
||||
start=datetime(2025, 9, 15, 10, 0, 0),
|
||||
end=datetime(2025, 9, 15, 11, 0, 0),
|
||||
subject="Mathématiques",
|
||||
group=None,
|
||||
content=None,
|
||||
),
|
||||
]
|
||||
provider = _StubProvider(theoretical_lessons)
|
||||
comparator = AgendaComparator(provider)
|
||||
result1 = comparator.compare(real_lessons, TARGET_DATE)
|
||||
result2 = comparator.compare(real_lessons, TARGET_DATE)
|
||||
assert result1 == result2
|
||||
Reference in New Issue
Block a user