""" Benchmark détaillé pour valider la migration get_assessment_statistics(). Vérifie les performances et l'exactitude de la migration étape 3.2. """ import pytest import time from datetime import date from models import Assessment, ClassGroup, Student, Exercise, GradingElement, Grade, db from config.feature_flags import FeatureFlag from app_config import config_manager class TestAssessmentStatisticsMigrationBenchmark: """Benchmark avancé de la migration des statistiques.""" def test_statistics_migration_correctness_complex_scenario(self, app): """ Test de validation avec un scénario complexe réaliste : - Évaluation avec 3 exercices - Mix de types de notation (notes et scores) - 15 étudiants avec scores variés et cas spéciaux """ with app.app_context(): # Créer des données de test réalistes assessment = self._create_realistic_assessment() # Test avec flag OFF (legacy) config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) start_time = time.perf_counter() legacy_stats = assessment.get_assessment_statistics() legacy_duration = time.perf_counter() - start_time # Test avec flag ON (refactored) config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: start_time = time.perf_counter() refactored_stats = assessment.get_assessment_statistics() refactored_duration = time.perf_counter() - start_time # Vérifications exactes print(f"\n📊 Statistiques complexes:") print(f" Legacy: {legacy_stats}") print(f" Refactored: {refactored_stats}") print(f"⏱️ Performance:") print(f" Legacy: {legacy_duration:.6f}s") print(f" Refactored: {refactored_duration:.6f}s") print(f" Ratio: {refactored_duration/legacy_duration:.2f}x") # Les résultats doivent être exactement identiques assert legacy_stats == refactored_stats, ( f"Mismatch detected!\nLegacy: {legacy_stats}\nRefactored: {refactored_stats}" ) # Les statistiques doivent être cohérentes assert legacy_stats['count'] == 15 # 15 étudiants assert legacy_stats['mean'] > 0 assert legacy_stats['min'] <= legacy_stats['mean'] <= legacy_stats['max'] assert legacy_stats['std_dev'] >= 0 # Le refactored ne doit pas être plus de 3x plus lent assert refactored_duration <= legacy_duration * 3, ( f"Performance regression! Refactored: {refactored_duration:.6f}s vs Legacy: {legacy_duration:.6f}s" ) finally: config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) def test_statistics_edge_cases_consistency(self, app): """Test des cas limites pour s'assurer de la cohérence.""" with app.app_context(): test_cases = [ self._create_assessment_all_zeros(), # Toutes les notes à 0 self._create_assessment_all_max(), # Toutes les notes maximales self._create_assessment_single_student(), # Un seul étudiant self._create_assessment_all_dispensed(), # Tous dispensés ] for i, assessment in enumerate(test_cases): print(f"\n🧪 Test case {i+1}: {assessment.title}") # Test legacy config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) legacy_stats = assessment.get_assessment_statistics() # Test refactored config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) try: refactored_stats = assessment.get_assessment_statistics() print(f" Legacy: {legacy_stats}") print(f" Refactored: {refactored_stats}") # Vérification exacte assert legacy_stats == refactored_stats, ( f"Case {i+1} failed: Legacy={legacy_stats}, Refactored={refactored_stats}" ) finally: config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) def test_statistics_performance_scaling(self, app): """Test de performance avec différentes tailles d'évaluations.""" with app.app_context(): sizes = [5, 10, 25] # Différentes tailles d'évaluations for size in sizes: print(f"\n⚡ Test performance avec {size} étudiants") assessment = self._create_assessment_with_n_students(size) # Mesures de performance legacy_times = [] refactored_times = [] # 3 mesures pour chaque version for _ in range(3): # Legacy config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) start = time.perf_counter() legacy_stats = assessment.get_assessment_statistics() legacy_times.append(time.perf_counter() - start) # Refactored config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', True) start = time.perf_counter() refactored_stats = assessment.get_assessment_statistics() refactored_times.append(time.perf_counter() - start) # Les résultats doivent toujours être identiques assert legacy_stats == refactored_stats # Moyenne des temps avg_legacy = sum(legacy_times) / len(legacy_times) avg_refactored = sum(refactored_times) / len(refactored_times) print(f" Legacy moyen: {avg_legacy:.6f}s") print(f" Refactored moyen: {avg_refactored:.6f}s") print(f" Amélioration: {avg_legacy/avg_refactored:.2f}x") config_manager.set('feature_flags.USE_REFACTORED_ASSESSMENT', False) # === Méthodes utilitaires de création de données === def _create_realistic_assessment(self): """Crée une évaluation complexe réaliste.""" # Classe avec 15 étudiants class_group = ClassGroup(name="6ème A", year="2025-2026") db.session.add(class_group) db.session.flush() students = [] for i in range(15): student = Student( first_name=f"Étudiant{i+1}", last_name=f"Test{i+1}", class_group_id=class_group.id ) students.append(student) db.session.add(student) db.session.flush() # Évaluation assessment = Assessment( title="Contrôle Complexe", description="Évaluation avec différents types de notation", date=date(2025, 1, 15), class_group_id=class_group.id, trimester=2, coefficient=2.0 ) db.session.add(assessment) db.session.flush() # Exercice 1 : Questions à points ex1 = Exercise(title="Calculs", assessment_id=assessment.id) db.session.add(ex1) db.session.flush() elem1 = GradingElement( label="Question 1a", exercise_id=ex1.id, max_points=8, grading_type="notes" ) db.session.add(elem1) db.session.flush() elem2 = GradingElement( label="Question 1b", exercise_id=ex1.id, max_points=12, grading_type="notes" ) db.session.add(elem2) db.session.flush() # Exercice 2 : Compétences ex2 = Exercise(title="Raisonnement", assessment_id=assessment.id) db.session.add(ex2) db.session.flush() elem3 = GradingElement( label="Raisonner", exercise_id=ex2.id, max_points=3, grading_type="score" ) db.session.add(elem3) db.session.flush() elem4 = GradingElement( label="Communiquer", exercise_id=ex2.id, max_points=3, grading_type="score" ) db.session.add(elem4) db.session.flush() # Notes variées avec distribution réaliste grades_to_add = [] import random for i, student in enumerate(students): # Question 1a : distribution normale autour de 6/8 score1a = max(0, min(8, random.gauss(6, 1.5))) grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem1.id, value=str(round(score1a, 1)))) # Question 1b : distribution normale autour de 9/12 score1b = max(0, min(12, random.gauss(9, 2))) grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem2.id, value=str(round(score1b, 1)))) # Compétences : distribution vers les niveaux moyens-élevés comp1 = random.choices([0, 1, 2, 3], weights=[1, 2, 4, 3])[0] comp2 = random.choices([0, 1, 2, 3], weights=[1, 3, 3, 2])[0] # Quelques cas spéciaux if i == 0: # Premier étudiant absent grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem3.id, value=".")) grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem4.id, value=".")) elif i == 1: # Deuxième étudiant dispensé grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem3.id, value="d")) grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem4.id, value=str(comp2))) else: # Notes normales grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem3.id, value=str(comp1))) grades_to_add.append(Grade(student_id=student.id, grading_element_id=elem4.id, value=str(comp2))) # Ajouter toutes les notes en une fois for grade in grades_to_add: db.session.add(grade) db.session.commit() return assessment def _create_assessment_all_zeros(self): """Évaluation avec toutes les notes à 0.""" class_group = ClassGroup(name="Test Zeros", year="2025-2026") db.session.add(class_group) db.session.flush() students = [Student(first_name=f"S{i}", last_name="Zero", class_group_id=class_group.id) for i in range(5)] for s in students: db.session.add(s) db.session.flush() assessment = Assessment( title="All Zeros Test", date=date(2025, 1, 15), class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.flush() ex = Exercise(title="Ex1", assessment_id=assessment.id) db.session.add(ex) db.session.flush() elem = GradingElement( label="Q1", exercise_id=ex.id, max_points=20, grading_type="notes" ) db.session.add(elem) db.session.flush() for student in students: grade = Grade(student_id=student.id, grading_element_id=elem.id, value="0") db.session.add(grade) db.session.commit() return assessment def _create_assessment_all_max(self): """Évaluation avec toutes les notes maximales.""" class_group = ClassGroup(name="Test Max", year="2025-2026") db.session.add(class_group) db.session.flush() students = [Student(first_name=f"S{i}", last_name="Max", class_group_id=class_group.id) for i in range(5)] for s in students: db.session.add(s) db.session.flush() assessment = Assessment( title="All Max Test", date=date(2025, 1, 15), class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.flush() ex = Exercise(title="Ex1", assessment_id=assessment.id) db.session.add(ex) db.session.flush() elem1 = GradingElement( label="Q1", exercise_id=ex.id, max_points=20, grading_type="notes" ) elem2 = GradingElement( label="C1", exercise_id=ex.id, max_points=3, grading_type="score" ) db.session.add_all([elem1, elem2]) db.session.flush() for student in students: grade1 = Grade(student_id=student.id, grading_element_id=elem1.id, value="20") grade2 = Grade(student_id=student.id, grading_element_id=elem2.id, value="3") db.session.add_all([grade1, grade2]) db.session.commit() return assessment def _create_assessment_single_student(self): """Évaluation avec un seul étudiant.""" class_group = ClassGroup(name="Test Single", year="2025-2026") db.session.add(class_group) db.session.flush() student = Student(first_name="Solo", last_name="Student", class_group_id=class_group.id) db.session.add(student) db.session.flush() assessment = Assessment( title="Single Student Test", date=date(2025, 1, 15), class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.flush() ex = Exercise(title="Ex1", assessment_id=assessment.id) db.session.add(ex) db.session.flush() elem = GradingElement( label="Q1", exercise_id=ex.id, max_points=10, grading_type="notes" ) db.session.add(elem) db.session.flush() grade = Grade(student_id=student.id, grading_element_id=elem.id, value="7.5") db.session.add(grade) db.session.commit() return assessment def _create_assessment_all_dispensed(self): """Évaluation avec tous les étudiants dispensés.""" class_group = ClassGroup(name="Test Dispensed", year="2025-2026") db.session.add(class_group) db.session.flush() students = [Student(first_name=f"S{i}", last_name="Dispensed", class_group_id=class_group.id) for i in range(3)] for s in students: db.session.add(s) db.session.flush() assessment = Assessment( title="All Dispensed Test", date=date(2025, 1, 15), class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.flush() ex = Exercise(title="Ex1", assessment_id=assessment.id) db.session.add(ex) db.session.flush() elem = GradingElement( label="Q1", exercise_id=ex.id, max_points=15, grading_type="notes" ) db.session.add(elem) db.session.flush() for student in students: grade = Grade(student_id=student.id, grading_element_id=elem.id, value="d") db.session.add(grade) db.session.commit() return assessment def _create_assessment_with_n_students(self, n): """Crée une évaluation avec n étudiants.""" class_group = ClassGroup(name=f"Test {n}S", year="2025-2026") db.session.add(class_group) db.session.flush() students = [] for i in range(n): student = Student(first_name=f"S{i}", last_name=f"Test{i}", class_group_id=class_group.id) students.append(student) db.session.add(student) db.session.flush() assessment = Assessment( title=f"Performance Test {n}", date=date(2025, 1, 15), class_group_id=class_group.id, trimester=1 ) db.session.add(assessment) db.session.flush() # 2 exercices avec plusieurs éléments for ex_num in range(2): ex = Exercise(title=f"Ex{ex_num+1}", assessment_id=assessment.id) db.session.add(ex) db.session.flush() for elem_num in range(3): elem = GradingElement( label=f"Q{elem_num+1}", exercise_id=ex.id, max_points=5 + elem_num * 2, grading_type="notes" ) db.session.add(elem) db.session.flush() # Notes aléatoires pour tous les étudiants import random for student in students: score = random.uniform(0.5, elem.max_points) grade = Grade( student_id=student.id, grading_element_id=elem.id, value=str(round(score, 1)) ) db.session.add(grade) db.session.commit() return assessment