Commit ·
4629595
1
Parent(s): 9416b5d
test: extended test suite - 64 more tests all passing (119 total)
Browse files- test_quran_extended.py +312 -0
test_quran_extended.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Extended test suite — Part 2
|
| 5 |
+
Additional edge cases beyond the 55 core tests.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import sys, os
|
| 9 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 10 |
+
from quran import search_bayan, normalize_arabic
|
| 11 |
+
import sqlite3, re
|
| 12 |
+
|
| 13 |
+
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'quran_master.db')
|
| 14 |
+
|
| 15 |
+
passed = 0
|
| 16 |
+
failed = 0
|
| 17 |
+
errors = []
|
| 18 |
+
|
| 19 |
+
def test(name, condition, detail=""):
|
| 20 |
+
global passed, failed, errors
|
| 21 |
+
if condition:
|
| 22 |
+
passed += 1
|
| 23 |
+
print(f" ✅ {name}")
|
| 24 |
+
else:
|
| 25 |
+
failed += 1
|
| 26 |
+
errors.append((name, detail))
|
| 27 |
+
print(f" ❌ {name} — {detail}")
|
| 28 |
+
|
| 29 |
+
def get(q, lang="تدقيق الايات"):
|
| 30 |
+
return search_bayan(q, lang, db_path=DB_PATH)
|
| 31 |
+
|
| 32 |
+
def ref_of(seg):
|
| 33 |
+
m = re.search(r'【([^】]+)】', seg)
|
| 34 |
+
return m.group(1) if m else ""
|
| 35 |
+
|
| 36 |
+
# ══════════════════════════════════════════
|
| 37 |
+
print("\n🔁 9. آيات متشابهة في سور مختلفة")
|
| 38 |
+
print("=" * 50)
|
| 39 |
+
|
| 40 |
+
# "الحمد لله" موجودة في الفاتحة والأنعام والكهف وسبأ وفاطر
|
| 41 |
+
# نتأكد إن النتيجة آية كاملة مش مقطوعة
|
| 42 |
+
r = get("الحمد لله الذي خلق السماوات والأرض")
|
| 43 |
+
seg = r.get("matched_segment", "")
|
| 44 |
+
ref = ref_of(seg)
|
| 45 |
+
test("آية متشابهة: اسم سورة موجود", len(ref) > 0, f"ref={ref}")
|
| 46 |
+
test("آية متشابهة: آية كاملة", "(١)" in seg or "(٢)" in seg, f"seg={seg[:60]}")
|
| 47 |
+
|
| 48 |
+
# "فبأي آلاء ربكما تكذبان" مكررة 31 مرة في الرحمن
|
| 49 |
+
r = get("فبأي آلاء ربكما تكذبان")
|
| 50 |
+
seg = r.get("matched_segment", "")
|
| 51 |
+
ref = ref_of(seg)
|
| 52 |
+
test("آية مكررة (فبأي آلاء): بترجع نتيجة", len(seg) > 10, f"ref={ref}")
|
| 53 |
+
|
| 54 |
+
# "ويل يومئذ للمكذبين" مكررة 10 مرات في المرسلات
|
| 55 |
+
r = get("ويل يومئذ للمكذبين")
|
| 56 |
+
seg = r.get("matched_segment", "")
|
| 57 |
+
test("آية مكررة (ويل): بترجع نتيجة", len(seg) > 10)
|
| 58 |
+
|
| 59 |
+
# ══════════════════════════════════════════
|
| 60 |
+
print("\n📏 10. آيات قصيرة جداً")
|
| 61 |
+
print("=" * 50)
|
| 62 |
+
|
| 63 |
+
# "طه" - كلمة واحدة
|
| 64 |
+
r = get("طه")
|
| 65 |
+
seg = r.get("matched_segment", "")
|
| 66 |
+
test("آية كلمة واحدة (طه): نتيجة", len(seg) > 5)
|
| 67 |
+
|
| 68 |
+
# "والفجر"
|
| 69 |
+
r = get("والفجر")
|
| 70 |
+
seg = r.get("matched_segment", "")
|
| 71 |
+
test("آية قصيرة (والفجر): نتيجة", len(seg) > 5)
|
| 72 |
+
|
| 73 |
+
# "والضحى"
|
| 74 |
+
r = get("والضحى")
|
| 75 |
+
seg = r.get("matched_segment", "")
|
| 76 |
+
test("آية قصيرة (والضحى): نتيجة", len(seg) > 5)
|
| 77 |
+
|
| 78 |
+
# "مدهامتان" (الرحمن 64)
|
| 79 |
+
r = get("مدهامتان")
|
| 80 |
+
seg = r.get("matched_segment", "")
|
| 81 |
+
test("آية كلمة واحدة (مدهامتان): نتيجة", len(seg) > 5)
|
| 82 |
+
|
| 83 |
+
# "والعصر"
|
| 84 |
+
r = get("والعصر")
|
| 85 |
+
seg = r.get("matched_segment", "")
|
| 86 |
+
ref = ref_of(seg)
|
| 87 |
+
test("والعصر: اسم سورة", "العصر" in ref or "عصر" in ref.lower(), f"ref={ref}")
|
| 88 |
+
|
| 89 |
+
# ══════════════════════════════════════════
|
| 90 |
+
print("\n🔤 11. كل الحروف المقطعة (29 سورة)")
|
| 91 |
+
print("=" * 50)
|
| 92 |
+
|
| 93 |
+
muqattaat = [
|
| 94 |
+
("الم", "البقرة/آل عمران"),
|
| 95 |
+
("المص", "الأعراف"),
|
| 96 |
+
("الر", "يونس/هود/يوسف"),
|
| 97 |
+
("المر", "الرعد"),
|
| 98 |
+
("كهيعص", "مريم"),
|
| 99 |
+
("طه", "طه"),
|
| 100 |
+
("طسم", "الشعراء/القصص"),
|
| 101 |
+
("طس", "النمل"),
|
| 102 |
+
("يس", "يس"),
|
| 103 |
+
("ص", "ص"),
|
| 104 |
+
("حم", "غافر وغيرها"),
|
| 105 |
+
("عسق", "الشورى"),
|
| 106 |
+
("ق", "ق"),
|
| 107 |
+
("ن", "القلم"),
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
for letters, sura in muqattaat:
|
| 111 |
+
r = get(letters)
|
| 112 |
+
seg = r.get("matched_segment", "")
|
| 113 |
+
has_result = len(seg) > 5 and "error" not in str(r)
|
| 114 |
+
test(f"حروف مقطعة '{letters}' ({sura})", has_result, f"seg={seg[:40]}")
|
| 115 |
+
|
| 116 |
+
# ══════════════════════════════════════════
|
| 117 |
+
print("\n🧹 12. تنظيف المدخلات")
|
| 118 |
+
print("=" * 50)
|
| 119 |
+
|
| 120 |
+
# مسافات زيادة
|
| 121 |
+
r = get(" قل هو الله أحد ")
|
| 122 |
+
seg = r.get("matched_segment", "")
|
| 123 |
+
test("مسافات زيادة: شغال", len(seg) > 10)
|
| 124 |
+
|
| 125 |
+
# تشكيل كامل في المدخل
|
| 126 |
+
r = get("قُلْ هُوَ اللَّهُ أَحَدٌ")
|
| 127 |
+
seg = r.get("matched_segment", "")
|
| 128 |
+
test("مدخل بتشكيل كامل: شغال", len(seg) > 10)
|
| 129 |
+
|
| 130 |
+
# بدون تشكيل خالص
|
| 131 |
+
r = get("قل هو الله احد")
|
| 132 |
+
seg = r.get("matched_segment", "")
|
| 133 |
+
test("مدخل بدون تشكيل: شغال", len(seg) > 10)
|
| 134 |
+
|
| 135 |
+
# همزات مختلفة
|
| 136 |
+
r = get("إنا أنزلناه في ليلة القدر")
|
| 137 |
+
seg1 = r.get("matched_segment", "")
|
| 138 |
+
r = get("انا انزلناه في ليله القدر")
|
| 139 |
+
seg2 = r.get("matched_segment", "")
|
| 140 |
+
test("همزات مختل��ة: نفس النتيجة", "القدر" in ref_of(seg1) and "القدر" in ref_of(seg2))
|
| 141 |
+
|
| 142 |
+
# تاء مربوطة vs هاء
|
| 143 |
+
r = get("ليله القدر")
|
| 144 |
+
seg = r.get("matched_segment", "")
|
| 145 |
+
test("تاء مربوطة/هاء: شغال", "القدر" in ref_of(seg), f"ref={ref_of(seg)}")
|
| 146 |
+
|
| 147 |
+
# ══════════════════════════════════════════
|
| 148 |
+
print("\n🌍 13. أسماء السور في التراجم")
|
| 149 |
+
print("=" * 50)
|
| 150 |
+
|
| 151 |
+
# English surah name
|
| 152 |
+
r = get("قل هو الله أحد", "english")
|
| 153 |
+
ref = ref_of(r.get("matched_segment", ""))
|
| 154 |
+
test("English: Surah name = Al-Ikhlas", "Ikhlas" in ref or "ikhlas" in ref.lower(), f"ref={ref}")
|
| 155 |
+
|
| 156 |
+
# French surah name
|
| 157 |
+
r = get("قل هو الله أحد", "french")
|
| 158 |
+
ref = ref_of(r.get("matched_segment", ""))
|
| 159 |
+
test("French: Surah name exists", len(ref) > 0, f"ref={ref}")
|
| 160 |
+
|
| 161 |
+
# Turkish surah name
|
| 162 |
+
r = get("قل هو الله أحد", "turkish")
|
| 163 |
+
ref = ref_of(r.get("matched_segment", ""))
|
| 164 |
+
test("Turkish: Surah name exists", len(ref) > 0, f"ref={ref}")
|
| 165 |
+
|
| 166 |
+
# Russian surah name
|
| 167 |
+
r = get("قل هو الله أحد", "russian")
|
| 168 |
+
ref = ref_of(r.get("matched_segment", ""))
|
| 169 |
+
test("Russian: Surah name exists", len(ref) > 0, f"ref={ref}")
|
| 170 |
+
|
| 171 |
+
# ══════════════════════════════════════════
|
| 172 |
+
print("\n🗃️ 14. سلامة البيانات المتقدمة")
|
| 173 |
+
print("=" * 50)
|
| 174 |
+
|
| 175 |
+
conn = sqlite3.connect(DB_PATH)
|
| 176 |
+
c = conn.cursor()
|
| 177 |
+
|
| 178 |
+
# لا توجد فجوات في ترقيم الآيات
|
| 179 |
+
c.execute("""
|
| 180 |
+
SELECT sura_num, MAX(aya_num) as max_aya, COUNT(*) as cnt
|
| 181 |
+
FROM verses
|
| 182 |
+
GROUP BY sura_num
|
| 183 |
+
HAVING max_aya != cnt
|
| 184 |
+
""")
|
| 185 |
+
gaps = c.fetchall()
|
| 186 |
+
test("ترقيم الآيات: مفيش فجوات", len(gaps) == 0, f"gaps={gaps}")
|
| 187 |
+
|
| 188 |
+
# كل آية ليها text_uthmani
|
| 189 |
+
c.execute("SELECT COUNT(*) FROM verses WHERE text_uthmani IS NULL OR text_uthmani = ''")
|
| 190 |
+
empty_uth = c.fetchone()[0]
|
| 191 |
+
test("text_uthmani مش فاضي لأي آية", empty_uth == 0, f"empty={empty_uth}")
|
| 192 |
+
|
| 193 |
+
# كل آية ليها ترجمة إنجليزية
|
| 194 |
+
c.execute("SELECT COUNT(*) FROM verses WHERE lang_en IS NULL OR lang_en = ''")
|
| 195 |
+
empty_en = c.fetchone()[0]
|
| 196 |
+
test("lang_en مش فاضي لأي آية", empty_en == 0, f"empty={empty_en}")
|
| 197 |
+
|
| 198 |
+
# كل آية ليها ترجمة فرنسية
|
| 199 |
+
c.execute("SELECT COUNT(*) FROM verses WHERE lang_fr IS NULL OR lang_fr = ''")
|
| 200 |
+
empty_fr = c.fetchone()[0]
|
| 201 |
+
test("lang_fr مش فاضي لأي آية", empty_fr == 0, f"empty={empty_fr}")
|
| 202 |
+
|
| 203 |
+
# أسماء السور بالعربي
|
| 204 |
+
c.execute("SELECT COUNT(*) FROM suras_translated WHERE ar IS NULL OR ar = ''")
|
| 205 |
+
empty_ar = c.fetchone()[0]
|
| 206 |
+
test("أسماء السور بالعربي: كلها موجودة", empty_ar == 0, f"empty={empty_ar}")
|
| 207 |
+
|
| 208 |
+
# أسماء السور بالإنجليزي
|
| 209 |
+
c.execute("SELECT COUNT(*) FROM suras_translated WHERE lang_en IS NULL OR lang_en = ''")
|
| 210 |
+
empty_en_s = c.fetchone()[0]
|
| 211 |
+
test("أسماء السور بالإنجليزي: كلها موجودة", empty_en_s == 0, f"empty={empty_en_s}")
|
| 212 |
+
|
| 213 |
+
# عدد آيات بعض السور المعروفة
|
| 214 |
+
known_counts = {1: 7, 2: 286, 3: 200, 9: 129, 12: 111, 36: 83, 55: 78, 97: 5, 108: 3, 112: 4, 114: 6}
|
| 215 |
+
for sura, expected in known_counts.items():
|
| 216 |
+
c.execute("SELECT COUNT(*) FROM verses WHERE sura_num = ?", (sura,))
|
| 217 |
+
actual = c.fetchone()[0]
|
| 218 |
+
test(f"سورة {sura}: {actual} آية (متوقع {expected})", actual == expected, f"actual={actual}")
|
| 219 |
+
|
| 220 |
+
conn.close()
|
| 221 |
+
|
| 222 |
+
# ══════════════════════════════════════════
|
| 223 |
+
print("\n🔀 15. البسملة في التراجم")
|
| 224 |
+
print("=" * 50)
|
| 225 |
+
|
| 226 |
+
# English: Basmala should be stripped from non-Fatiha
|
| 227 |
+
r = get("الر تلك آيات الكتاب المبين", "english")
|
| 228 |
+
seg = r.get("matched_segment", "")
|
| 229 |
+
test("English: بسملة مش موجودة", "In the name of" not in seg, f"seg={seg[:60]}")
|
| 230 |
+
|
| 231 |
+
# French: same
|
| 232 |
+
r = get("الر تلك آيات الكتاب المبين", "french")
|
| 233 |
+
seg = r.get("matched_segment", "")
|
| 234 |
+
test("French: بسملة مش موجودة", "Au nom d" not in seg and "Bismillah" not in seg.lower(), f"seg={seg[:60]}")
|
| 235 |
+
|
| 236 |
+
# ══════════════════════════════════════════
|
| 237 |
+
print("\n⚡ 16. أداء وحالات حدّية")
|
| 238 |
+
print("=" * 50)
|
| 239 |
+
|
| 240 |
+
# نص طويل جداً (أول ربع من البقرة)
|
| 241 |
+
long_text = "الم ذلك الكتاب لا ريب فيه هدى للمتقين الذين يؤمنون بالغيب ويقيمون الصلاة ومما رزقناهم ينفقون والذين يؤمنون بما انزل اليك وما انزل من قبلك وبالآخرة هم يوقنون اولئك على هدى من ربهم واولئك هم المفلحون"
|
| 242 |
+
r = get(long_text)
|
| 243 |
+
seg = r.get("matched_segment", "")
|
| 244 |
+
test("نص طويل: بيرجع نتيجة", len(seg) > 50, f"len={len(seg)}")
|
| 245 |
+
test("نص طويل: سورة البقرة", "البقرة" in ref_of(seg) or "بقر" in ref_of(seg).lower(), f"ref={ref_of(seg)}")
|
| 246 |
+
|
| 247 |
+
# نص فيه أرقام
|
| 248 |
+
r = get("123 الله")
|
| 249 |
+
test("نص فيه أرقام: مش بيكسر", True) # should not crash
|
| 250 |
+
|
| 251 |
+
# علامات ترقيم
|
| 252 |
+
r = get("قل هو الله أحد!!!")
|
| 253 |
+
seg = r.get("matched_segment", "")
|
| 254 |
+
test("علامات ترقيم: شغال", len(seg) > 5)
|
| 255 |
+
|
| 256 |
+
# تكرار نفس الكلمة
|
| 257 |
+
r = get("الله الله الله")
|
| 258 |
+
seg = r.get("matched_segment", "")
|
| 259 |
+
test("تكرار كلمة: مش بيكسر", True)
|
| 260 |
+
|
| 261 |
+
# حرف واحد
|
| 262 |
+
r = get("ق")
|
| 263 |
+
seg = r.get("matched_segment", "")
|
| 264 |
+
test("حرف واحد (ق): شغال", len(seg) > 5)
|
| 265 |
+
|
| 266 |
+
# حرف واحد (ص)
|
| 267 |
+
r = get("ص")
|
| 268 |
+
seg = r.get("matched_segment", "")
|
| 269 |
+
test("حرف واحد (ص): شغال", len(seg) > 5)
|
| 270 |
+
|
| 271 |
+
# حرف واحد (ن)
|
| 272 |
+
r = get("ن")
|
| 273 |
+
seg = r.get("matched_segment", "")
|
| 274 |
+
test("حرف واحد (ن): شغال", len(seg) > 5)
|
| 275 |
+
|
| 276 |
+
# ══════════════════════════════════════════
|
| 277 |
+
print("\n🎯 17. دقة التطابق")
|
| 278 |
+
print("=" * 50)
|
| 279 |
+
|
| 280 |
+
# آية الكرسي
|
| 281 |
+
r = get("الله لا اله الا هو الحي القيوم لا تأخذه سنة ولا نوم")
|
| 282 |
+
seg = r.get("matched_segment", "")
|
| 283 |
+
ref = ref_of(seg)
|
| 284 |
+
test("آية الكرسي: البقرة ٢٥٥", "البقرة" in ref or "بقر" in ref.lower(), f"ref={ref}")
|
| 285 |
+
test("آية الكرسي: رقم ٢٥٥", "٢٥٥" in ref or "255" in ref, f"ref={ref}")
|
| 286 |
+
|
| 287 |
+
# خواتيم البقرة
|
| 288 |
+
r = get("آمن الرسول بما انزل اليه من ربه والمؤمنون")
|
| 289 |
+
seg = r.get("matched_segment", "")
|
| 290 |
+
ref = ref_of(seg)
|
| 291 |
+
test("خواتيم البقرة: ٢٨٥", "٢٨٥" in ref or "285" in ref, f"ref={ref}")
|
| 292 |
+
|
| 293 |
+
# آية النور ٣٥ (آية النور)
|
| 294 |
+
r = get("الله نور السماوات والأرض مثل نوره كمشكاة")
|
| 295 |
+
seg = r.get("matched_segment", "")
|
| 296 |
+
ref = ref_of(seg)
|
| 297 |
+
test("آية النور: سورة النور", "النور" in ref or "نور" in ref.lower(), f"ref={ref}")
|
| 298 |
+
|
| 299 |
+
# سورة يس آية 82
|
| 300 |
+
r = get("انما امره اذا اراد شيئا ان يقول له كن فيكون")
|
| 301 |
+
seg = r.get("matched_segment", "")
|
| 302 |
+
test("كن فيكون: نتيجة صحيحة", len(seg) > 20)
|
| 303 |
+
|
| 304 |
+
# ══════════════════════════════════════════
|
| 305 |
+
print("\n" + "=" * 60)
|
| 306 |
+
print(f"📊 النتيجة: {passed} ✅ نجح | {failed} ❌ فشل | من {passed + failed} اختبار")
|
| 307 |
+
print("=" * 60)
|
| 308 |
+
|
| 309 |
+
if errors:
|
| 310 |
+
print("\n❌ الاختبارات الفاشلة:")
|
| 311 |
+
for name, detail in errors:
|
| 312 |
+
print(f" • {name}: {detail}")
|