Delete tests
Browse files- tests/README.md +0 -130
- tests/integration/test_dashboard.py +0 -266
- tests/integration/test_report_export.py +0 -246
- tests/test_app.py +0 -601
- tests/test_integrated_system.py +0 -96
- tests/test_ui.py +0 -413
- tests/unit/test_reports.py +0 -200
tests/README.md
DELETED
@@ -1,130 +0,0 @@
|
|
1 |
-
# اختبارات نظام تسعير المناقصات
|
2 |
-
|
3 |
-
يحتوي هذا المجلد على اختبارات وحدة واختبارات تكامل لنظام تسعير المناقصات.
|
4 |
-
|
5 |
-
## هيكل المجلد
|
6 |
-
|
7 |
-
- `unit/`: اختبارات الوحدة للمكونات الفردية
|
8 |
-
- `integration/`: اختبارات التكامل بين مكونات النظام المختلفة
|
9 |
-
|
10 |
-
## تشغيل الاختبارات
|
11 |
-
|
12 |
-
### تشغيل جميع الاختبارات
|
13 |
-
|
14 |
-
لتشغيل جميع الاختبارات، قم بتنفيذ الأمر التالي من المجلد الرئيسي للمشروع:
|
15 |
-
|
16 |
-
```bash
|
17 |
-
python -m unittest discover -s tests
|
18 |
-
```
|
19 |
-
|
20 |
-
### تشغيل اختبارات الوحدة فقط
|
21 |
-
|
22 |
-
```bash
|
23 |
-
python -m unittest discover -s tests/unit
|
24 |
-
```
|
25 |
-
|
26 |
-
### تشغيل اختبارات التكامل فقط
|
27 |
-
|
28 |
-
```bash
|
29 |
-
python -m unittest discover -s tests/integration
|
30 |
-
```
|
31 |
-
|
32 |
-
### تشغيل ملف اختبار محدد
|
33 |
-
|
34 |
-
```bash
|
35 |
-
python -m unittest tests/unit/test_reports.py
|
36 |
-
```
|
37 |
-
|
38 |
-
## كتابة اختبارات جديدة
|
39 |
-
|
40 |
-
### اختبارات الوحدة
|
41 |
-
|
42 |
-
اختبارات الوحدة تركز على اختبار مكون واحد معزول عن باقي النظام. يجب اتباع النمط التالي:
|
43 |
-
|
44 |
-
```python
|
45 |
-
import unittest
|
46 |
-
|
47 |
-
class TestComponentName(unittest.TestCase):
|
48 |
-
|
49 |
-
def setUp(self):
|
50 |
-
# إعداد بيئة الاختبار
|
51 |
-
pass
|
52 |
-
|
53 |
-
def tearDown(self):
|
54 |
-
# تنظيف بيئة الاختبار
|
55 |
-
pass
|
56 |
-
|
57 |
-
def test_functionality_name(self):
|
58 |
-
# اختبار وظيفة معينة
|
59 |
-
result = function_to_test(params)
|
60 |
-
self.assertEqual(result, expected_value)
|
61 |
-
```
|
62 |
-
|
63 |
-
### اختبارات التكامل
|
64 |
-
|
65 |
-
اختبارات التكامل تركز على التفاعل بين مكونين أو أكثر. يجب اتباع النمط التالي:
|
66 |
-
|
67 |
-
```python
|
68 |
-
import unittest
|
69 |
-
|
70 |
-
class TestIntegrationName(unittest.TestCase):
|
71 |
-
|
72 |
-
def setUp(self):
|
73 |
-
# إعداد بيئة الاختبار
|
74 |
-
# إنشاء المكونات المختلفة المطلوبة
|
75 |
-
pass
|
76 |
-
|
77 |
-
def tearDown(self):
|
78 |
-
# تنظيف بيئة الاختبار
|
79 |
-
pass
|
80 |
-
|
81 |
-
def test_integration_scenario(self):
|
82 |
-
# اختبار سيناريو تكامل محدد
|
83 |
-
result = component1.action(params)
|
84 |
-
component2.process(result)
|
85 |
-
final_result = component2.get_result()
|
86 |
-
self.assertEqual(final_result, expected_value)
|
87 |
-
```
|
88 |
-
|
89 |
-
## المحاكاة والاستبدال
|
90 |
-
|
91 |
-
في بعض الحالات، قد نحتاج إلى محاكاة بعض المكونات لعزل المكون الذي نختبره. يمكن استخدام مكتبة `unittest.mock` لهذا الغرض:
|
92 |
-
|
93 |
-
```python
|
94 |
-
from unittest.mock import Mock, patch
|
95 |
-
|
96 |
-
class TestWithMocking(unittest.TestCase):
|
97 |
-
|
98 |
-
@patch('module.ClassName')
|
99 |
-
def test_with_mock(self, MockClass):
|
100 |
-
# إعداد المحاكاة
|
101 |
-
instance = MockClass.return_value
|
102 |
-
instance.method.return_value = 'mocked_result'
|
103 |
-
|
104 |
-
# استدعاء الدالة التي تستخدم الكائن المحاكى
|
105 |
-
result = function_to_test()
|
106 |
-
|
107 |
-
# التحقق من النتائج ومن استدعاء المحاكاة
|
108 |
-
self.assertEqual(result, expected_value)
|
109 |
-
instance.method.assert_called_once_with(expected_params)
|
110 |
-
```
|
111 |
-
|
112 |
-
## التغطية
|
113 |
-
|
114 |
-
يمكن قياس تغطية الاختبارات باستخدام أداة `coverage`:
|
115 |
-
|
116 |
-
```bash
|
117 |
-
# تثبيت أداة coverage
|
118 |
-
pip install coverage
|
119 |
-
|
120 |
-
# تشغيل الاختبارات مع قياس التغطية
|
121 |
-
coverage run -m unittest discover -s tests
|
122 |
-
|
123 |
-
# عرض تقرير التغطية
|
124 |
-
coverage report
|
125 |
-
|
126 |
-
# إنشاء تقرير HTML للتغطية
|
127 |
-
coverage html
|
128 |
-
```
|
129 |
-
|
130 |
-
ملف تقرير HTML سيكون في مجلد `htmlcov/index.html`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/integration/test_dashboard.py
DELETED
@@ -1,266 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
اختبارات تكامل للوحة المعلومات
|
3 |
-
"""
|
4 |
-
|
5 |
-
import unittest
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
import os
|
9 |
-
import sys
|
10 |
-
import json
|
11 |
-
from datetime import datetime, timedelta
|
12 |
-
|
13 |
-
# إضافة المسار الرئيسي للمشروع لاستيراد الوحدات
|
14 |
-
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
15 |
-
|
16 |
-
from modules.reports.reports_app import ReportsApp
|
17 |
-
import streamlit as st
|
18 |
-
|
19 |
-
|
20 |
-
class TestDashboard(unittest.TestCase):
|
21 |
-
"""اختبارات تكامل للوحة المعلومات"""
|
22 |
-
|
23 |
-
def setUp(self):
|
24 |
-
"""إعداد بيئة الاختبار"""
|
25 |
-
self.reports_app = ReportsApp()
|
26 |
-
|
27 |
-
# إنشاء بيانات محاكاة للمشاريع
|
28 |
-
self.mock_projects = [
|
29 |
-
{
|
30 |
-
'id': 1,
|
31 |
-
'name': 'مشروع 1',
|
32 |
-
'number': 'T-2024001',
|
33 |
-
'client': 'وزارة الصحة',
|
34 |
-
'location': 'الرياض',
|
35 |
-
'status': 'جديد',
|
36 |
-
'submission_date': '2024-01-15',
|
37 |
-
'tender_type': 'عامة',
|
38 |
-
'created_at': '2024-01-10',
|
39 |
-
'value': 1500000,
|
40 |
-
'local_content': 75
|
41 |
-
},
|
42 |
-
{
|
43 |
-
'id': 2,
|
44 |
-
'name': 'مشروع 2',
|
45 |
-
'number': 'T-2024002',
|
46 |
-
'client': 'وزارة التعليم',
|
47 |
-
'location': 'جدة',
|
48 |
-
'status': 'قيد التسعير',
|
49 |
-
'submission_date': '2024-01-28',
|
50 |
-
'tender_type': 'خاصة',
|
51 |
-
'created_at': '2024-01-20',
|
52 |
-
'value': 4500000,
|
53 |
-
'local_content': 80
|
54 |
-
},
|
55 |
-
{
|
56 |
-
'id': 3,
|
57 |
-
'name': 'مشروع 3',
|
58 |
-
'number': 'T-2024003',
|
59 |
-
'client': 'وزارة الصحة',
|
60 |
-
'location': 'الدمام',
|
61 |
-
'status': 'تم التقديم',
|
62 |
-
'submission_date': '2024-02-10',
|
63 |
-
'tender_type': 'عامة',
|
64 |
-
'created_at': '2024-02-01',
|
65 |
-
'value': 8000000,
|
66 |
-
'local_content': 70
|
67 |
-
},
|
68 |
-
{
|
69 |
-
'id': 4,
|
70 |
-
'name': 'مشروع 4',
|
71 |
-
'number': 'T-2024004',
|
72 |
-
'client': 'شركة أرامكو',
|
73 |
-
'location': 'الظهران',
|
74 |
-
'status': 'تمت الترسية',
|
75 |
-
'submission_date': '2024-02-15',
|
76 |
-
'tender_type': 'عامة',
|
77 |
-
'created_at': '2024-02-05',
|
78 |
-
'value': 15000000,
|
79 |
-
'local_content': 85
|
80 |
-
},
|
81 |
-
{
|
82 |
-
'id': 5,
|
83 |
-
'name': 'مشروع 5',
|
84 |
-
'number': 'T-2024005',
|
85 |
-
'client': 'شركة سابك',
|
86 |
-
'location': 'الجبيل',
|
87 |
-
'status': 'قيد التنفيذ',
|
88 |
-
'submission_date': '2024-02-20',
|
89 |
-
'tender_type': 'خاصة',
|
90 |
-
'created_at': '2024-02-10',
|
91 |
-
'value': 25000000,
|
92 |
-
'local_content': 75
|
93 |
-
},
|
94 |
-
{
|
95 |
-
'id': 6,
|
96 |
-
'name': 'مشروع 6',
|
97 |
-
'number': 'T-2024006',
|
98 |
-
'client': 'وزارة النقل',
|
99 |
-
'location': 'الرياض',
|
100 |
-
'status': 'منتهي',
|
101 |
-
'submission_date': '2024-01-05',
|
102 |
-
'tender_type': 'عامة',
|
103 |
-
'created_at': '2023-12-20',
|
104 |
-
'value': 5000000,
|
105 |
-
'local_content': 72
|
106 |
-
},
|
107 |
-
{
|
108 |
-
'id': 7,
|
109 |
-
'name': 'مشروع 7',
|
110 |
-
'number': 'T-2024007',
|
111 |
-
'client': 'وزارة الإسكان',
|
112 |
-
'location': 'مكة',
|
113 |
-
'status': 'ملغي',
|
114 |
-
'submission_date': '2024-01-10',
|
115 |
-
'tender_type': 'خاصة',
|
116 |
-
'created_at': '2023-12-25',
|
117 |
-
'value': 12000000,
|
118 |
-
'local_content': 65
|
119 |
-
}
|
120 |
-
]
|
121 |
-
|
122 |
-
# تعيين المشاريع في حالة الجلسة
|
123 |
-
st.session_state.projects = self.mock_projects
|
124 |
-
|
125 |
-
def test_dashboard_metrics(self):
|
126 |
-
"""اختبار مؤشرات لوحة المعلومات الرئيسية"""
|
127 |
-
# الحصول على المؤشرات
|
128 |
-
total_projects = self.reports_app._get_total_projects()
|
129 |
-
active_projects = self.reports_app._get_active_projects()
|
130 |
-
won_projects = self.reports_app._get_won_projects()
|
131 |
-
avg_local_content = self.reports_app._get_avg_local_content()
|
132 |
-
|
133 |
-
# التحقق من المؤشرات
|
134 |
-
self.assertEqual(total_projects, 7)
|
135 |
-
self.assertEqual(active_projects, 4) # قيد التسعير، تم التقديم، تمت الترسية، قيد التنفيذ
|
136 |
-
self.assertEqual(won_projects, 3) # تمت الترسية، قيد التنفيذ، منتهي
|
137 |
-
|
138 |
-
# التحقق من أن متوسط المحتوى المحلي قريب من القيمة المتوقعة
|
139 |
-
expected_avg_local_content = sum(p['local_content'] for p in self.mock_projects) / len(self.mock_projects)
|
140 |
-
self.assertAlmostEqual(avg_local_content, expected_avg_local_content, delta=1.0)
|
141 |
-
|
142 |
-
def test_project_status_distribution(self):
|
143 |
-
"""اختبار توزيع المشاريع حسب الحالة"""
|
144 |
-
# الحصول على بيانات التوزيع
|
145 |
-
status_data = self.reports_app._get_project_status_data()
|
146 |
-
|
147 |
-
# التحقق من أن البيانات إطار بيانات
|
148 |
-
self.assertIsInstance(status_data, pd.DataFrame)
|
149 |
-
|
150 |
-
# التحقق من الأعمدة
|
151 |
-
self.assertIn('status', status_data.columns)
|
152 |
-
self.assertIn('count', status_data.columns)
|
153 |
-
|
154 |
-
# التحقق من أن عدد الحالات الفريدة يساوي 7
|
155 |
-
self.assertEqual(len(status_data), 7)
|
156 |
-
|
157 |
-
# التحقق من أن مجموع المشاريع صحيح
|
158 |
-
self.assertEqual(status_data['count'].sum(), 7)
|
159 |
-
|
160 |
-
# التحقق من عدد المشاريع في كل حالة
|
161 |
-
status_counts = status_data.set_index('status')['count'].to_dict()
|
162 |
-
self.assertEqual(status_counts['جديد'], 1)
|
163 |
-
self.assertEqual(status_counts['قيد التسعير'], 1)
|
164 |
-
self.assertEqual(status_counts['تم التقديم'], 1)
|
165 |
-
self.assertEqual(status_counts['تمت الترسية'], 1)
|
166 |
-
self.assertEqual(status_counts['قيد التنفيذ'], 1)
|
167 |
-
self.assertEqual(status_counts['منتهي'], 1)
|
168 |
-
self.assertEqual(status_counts['ملغي'], 1)
|
169 |
-
|
170 |
-
def test_monthly_trend_data(self):
|
171 |
-
"""اختبار بيانات الاتجاه الشهري"""
|
172 |
-
# الحصول على بيانات الاتجاه
|
173 |
-
monthly_data = self.reports_app._get_monthly_project_data()
|
174 |
-
|
175 |
-
# التحقق من أن البيانات إطار بيانات
|
176 |
-
self.assertIsInstance(monthly_data, pd.DataFrame)
|
177 |
-
|
178 |
-
# التحقق من الأعمدة
|
179 |
-
self.assertIn('month', monthly_data.columns)
|
180 |
-
self.assertIn('new', monthly_data.columns)
|
181 |
-
self.assertIn('submitted', monthly_data.columns)
|
182 |
-
self.assertIn('won', monthly_data.columns)
|
183 |
-
|
184 |
-
# التحقق من أن عدد الأشهر يساوي 6
|
185 |
-
self.assertEqual(len(monthly_data), 6)
|
186 |
-
|
187 |
-
def test_project_value_distribution(self):
|
188 |
-
"""اختبار توزيع المشاريع حسب القيمة"""
|
189 |
-
# الحصول على بيانات التوزيع
|
190 |
-
value_data = self.reports_app._get_project_value_data()
|
191 |
-
|
192 |
-
# التحقق من أن البيانات إطار بيانات
|
193 |
-
self.assertIsInstance(value_data, pd.DataFrame)
|
194 |
-
|
195 |
-
# التحقق من الأعمدة
|
196 |
-
self.assertIn('range', value_data.columns)
|
197 |
-
self.assertIn('count', value_data.columns)
|
198 |
-
|
199 |
-
# التحقق من أن عدد النطاقات يساوي 7
|
200 |
-
self.assertEqual(len(value_data), 7)
|
201 |
-
|
202 |
-
# التحقق من أن عدد المشاريع صحيح
|
203 |
-
self.assertEqual(value_data['count'].sum(), 7)
|
204 |
-
|
205 |
-
# التحقق من نطاقات القيمة
|
206 |
-
value_ranges = value_data['range'].tolist()
|
207 |
-
self.assertIn('أقل من 1 مليون', value_ranges)
|
208 |
-
self.assertIn('1-5 مليون', value_ranges)
|
209 |
-
self.assertIn('5-10 مليون', value_ranges)
|
210 |
-
self.assertIn('10-20 مليون', value_ranges)
|
211 |
-
self.assertIn('20-50 مليون', value_ranges)
|
212 |
-
|
213 |
-
def test_custom_report_generation(self):
|
214 |
-
"""اختبار إنشاء التقارير المخصصة"""
|
215 |
-
# اختبار إنشاء تقرير جدول
|
216 |
-
table_data = self.reports_app._generate_sample_data(
|
217 |
-
data_source="المشاريع",
|
218 |
-
fields=["اسم المشروع", "العميل", "الحالة", "تاريخ التقديم"],
|
219 |
-
rows=10
|
220 |
-
)
|
221 |
-
|
222 |
-
# التحقق من النتائج
|
223 |
-
self.assertIsInstance(table_data, pd.DataFrame)
|
224 |
-
self.assertEqual(len(table_data), 10)
|
225 |
-
self.assertEqual(len(table_data.columns), 4)
|
226 |
-
|
227 |
-
# اختبار فلترة البيانات
|
228 |
-
filters = [
|
229 |
-
{
|
230 |
-
'field': 'العميل',
|
231 |
-
'operator': 'يساوي',
|
232 |
-
'value': 'وزارة الصحة'
|
233 |
-
}
|
234 |
-
]
|
235 |
-
|
236 |
-
filtered_data = table_data.copy()
|
237 |
-
for filter_info in filters:
|
238 |
-
field = filter_info['field']
|
239 |
-
operator = filter_info['operator']
|
240 |
-
value = filter_info['value']
|
241 |
-
|
242 |
-
if field in filtered_data.columns:
|
243 |
-
if operator == "يساوي":
|
244 |
-
filtered_data = filtered_data[filtered_data[field] == value]
|
245 |
-
|
246 |
-
# التحقق من أن الفلترة تعمل بشكل صحيح
|
247 |
-
if not filtered_data.empty:
|
248 |
-
self.assertTrue(all(client == 'وزارة الصحة' for client in filtered_data['العميل']))
|
249 |
-
|
250 |
-
def test_report_export_functionality(self):
|
251 |
-
"""اختبار وظيفة تصدير التقارير"""
|
252 |
-
# في هذا الاختبار نفترض وجود وظيفة تصدير تعمل بشكل صحيح
|
253 |
-
# ونتحقق فقط من أن البيانات جاهزة للتصدير
|
254 |
-
|
255 |
-
# الحصول على بيانات المشاريع
|
256 |
-
project_data = pd.DataFrame(self.mock_projects)
|
257 |
-
|
258 |
-
# التحقق من أن البيانات جاهزة للتصدير
|
259 |
-
self.assertTrue(len(project_data) > 0)
|
260 |
-
self.assertIn('name', project_data.columns)
|
261 |
-
self.assertIn('client', project_data.columns)
|
262 |
-
self.assertIn('status', project_data.columns)
|
263 |
-
|
264 |
-
|
265 |
-
if __name__ == '__main__':
|
266 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/integration/test_report_export.py
DELETED
@@ -1,246 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
اختبارات تكامل لتصدير التقارير
|
3 |
-
"""
|
4 |
-
|
5 |
-
import unittest
|
6 |
-
import pandas as pd
|
7 |
-
import os
|
8 |
-
import sys
|
9 |
-
import tempfile
|
10 |
-
import shutil
|
11 |
-
from datetime import datetime
|
12 |
-
|
13 |
-
# إضافة المسار الرئيسي للمشروع لاستيراد الوحدات
|
14 |
-
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
15 |
-
|
16 |
-
from modules.reports.reports_app import ReportsApp
|
17 |
-
from utils.excel_handler import export_to_excel
|
18 |
-
|
19 |
-
|
20 |
-
class TestReportExport(unittest.TestCase):
|
21 |
-
"""اختبارات تكامل لتصدير التقارير"""
|
22 |
-
|
23 |
-
def setUp(self):
|
24 |
-
"""إعداد بيئة الاختبار"""
|
25 |
-
self.reports_app = ReportsApp()
|
26 |
-
|
27 |
-
# إنشاء مجلد مؤقت للملفات المصدرة
|
28 |
-
self.test_dir = tempfile.mkdtemp()
|
29 |
-
|
30 |
-
# إنشاء بيانات محاكاة للمشاريع
|
31 |
-
self.projects_data = pd.DataFrame({
|
32 |
-
'اسم المشروع': [f'مشروع {i}' for i in range(1, 6)],
|
33 |
-
'رقم المناقصة': [f'T-2024{i:03d}' for i in range(1, 6)],
|
34 |
-
'العميل': ['وزارة الصحة', 'وزارة التعليم', 'وزارة الصحة', 'شركة أرامكو', 'شركة سابك'],
|
35 |
-
'الموقع': ['الرياض', 'جدة', 'الدمام', 'الظهران', 'الجبيل'],
|
36 |
-
'الحالة': ['جديد', 'قيد التسعير', 'تم التقديم', 'تمت الترسية', 'قيد التنفيذ'],
|
37 |
-
'تاريخ التقديم': ['2024-01-15', '2024-01-28', '2024-02-10', '2024-02-15', '2024-02-20'],
|
38 |
-
'نوع المناقصة': ['عامة', 'خاصة', 'عامة', 'عامة', 'خاصة']
|
39 |
-
})
|
40 |
-
|
41 |
-
# إنشاء بيانات محاكاة للتسعير
|
42 |
-
self.pricing_data = pd.DataFrame({
|
43 |
-
'اسم المشروع': ['مشروع 1', 'مشروع 1', 'مشروع 1', 'مشروع 2', 'مشروع 2'],
|
44 |
-
'رقم البند': ['A001', 'A002', 'A003', 'B001', 'B002'],
|
45 |
-
'وصف البند': [
|
46 |
-
'أعمال الحفر والردم',
|
47 |
-
'أعمال الخرسانة المسلحة',
|
48 |
-
'أعمال حديد التسليح',
|
49 |
-
'أعمال البلوك',
|
50 |
-
'أعمال اللياسة'
|
51 |
-
],
|
52 |
-
'الوحدة': ['م3', 'م3', 'طن', 'م2', 'م2'],
|
53 |
-
'الكمية': [1000, 500, 50, 800, 1200],
|
54 |
-
'سعر الوحدة': [50, 1200, 5000, 120, 80],
|
55 |
-
'الإجمالي': [50000, 600000, 250000, 96000, 96000]
|
56 |
-
})
|
57 |
-
|
58 |
-
# إنشاء بيانات محاكاة للمخاطر
|
59 |
-
self.risks_data = pd.DataFrame({
|
60 |
-
'رمز المخاطرة': [f'R{i:03d}' for i in range(1, 6)],
|
61 |
-
'اسم المشروع': ['مشروع 1', 'مشروع 1', 'مشروع 2', 'مشروع 3', 'مشروع 4'],
|
62 |
-
'وصف المخاطرة': [
|
63 |
-
'غرامة تأخير مرتفعة',
|
64 |
-
'مدة تنفيذ قصيرة',
|
65 |
-
'متطلبات ضمان بنكي مرتفعة',
|
66 |
-
'شروط دفع متأخرة',
|
67 |
-
'متطلبات تأمين شاملة ومكلفة'
|
68 |
-
],
|
69 |
-
'الفئة': [
|
70 |
-
'مخاطر مالية',
|
71 |
-
'مخاطر زمنية',
|
72 |
-
'مخاطر مالية',
|
73 |
-
'مخاطر مالية',
|
74 |
-
'مخاطر مالية'
|
75 |
-
],
|
76 |
-
'التأثير': ['عالي', 'متوسط', 'منخفض', 'عالي', 'متوسط'],
|
77 |
-
'الاحتمالية': ['محتمل', 'مؤكد', 'غير محتمل', 'محتمل', 'غير محتمل'],
|
78 |
-
'درجة المخاطرة': [6, 6, 2, 8, 4]
|
79 |
-
})
|
80 |
-
|
81 |
-
def tearDown(self):
|
82 |
-
"""تنظيف بيئة الاختبار"""
|
83 |
-
# حذف المجلد المؤقت
|
84 |
-
shutil.rmtree(self.test_dir)
|
85 |
-
|
86 |
-
def test_export_projects_report_to_excel(self):
|
87 |
-
"""اختبار تصدير تقرير المشاريع إلى Excel"""
|
88 |
-
# إنشاء مسار الملف المصدر
|
89 |
-
output_file = os.path.join(self.test_dir, 'projects_report.xlsx')
|
90 |
-
|
91 |
-
# تصدير البيانات
|
92 |
-
result = export_to_excel(self.projects_data, output_file, sheet_name='المشاريع', title='تقرير المشاريع')
|
93 |
-
|
94 |
-
# التحقق من نجاح التصدير
|
95 |
-
self.assertTrue(result)
|
96 |
-
self.assertTrue(os.path.exists(output_file))
|
97 |
-
|
98 |
-
# التحقق من محتوى الملف المصدر
|
99 |
-
imported_data = pd.read_excel(output_file, sheet_name='المشاريع', skiprows=2) # تخطي الصفوف الأولى (العنوان)
|
100 |
-
|
101 |
-
# التحقق من عدد الصفوف والأعمدة
|
102 |
-
self.assertEqual(len(imported_data), len(self.projects_data))
|
103 |
-
self.assertEqual(len(imported_data.columns), len(self.projects_data.columns))
|
104 |
-
|
105 |
-
def test_export_pricing_report_to_excel(self):
|
106 |
-
"""اختبار تصدير تقرير التسعير إلى Excel"""
|
107 |
-
# إنشاء مسار الملف المصدر
|
108 |
-
output_file = os.path.join(self.test_dir, 'pricing_report.xlsx')
|
109 |
-
|
110 |
-
# تصدير البيانات
|
111 |
-
result = export_to_excel(self.pricing_data, output_file, sheet_name='التسعير', title='تقرير التسعير')
|
112 |
-
|
113 |
-
# التحقق من نجاح التصدير
|
114 |
-
self.assertTrue(result)
|
115 |
-
self.assertTrue(os.path.exists(output_file))
|
116 |
-
|
117 |
-
# التحقق من محتوى الملف المصدر
|
118 |
-
imported_data = pd.read_excel(output_file, sheet_name='التسعير', skiprows=2) # تخطي الصفوف الأولى (العنوان)
|
119 |
-
|
120 |
-
# التحقق من عدد الصفوف والأعمدة
|
121 |
-
self.assertEqual(len(imported_data), len(self.pricing_data))
|
122 |
-
self.assertEqual(len(imported_data.columns), len(self.pricing_data.columns))
|
123 |
-
|
124 |
-
def test_export_multiple_sheets(self):
|
125 |
-
"""اختبار تصدير تقرير متعدد الصفحات"""
|
126 |
-
# إنشاء مسار الملف المصدر
|
127 |
-
output_file = os.path.join(self.test_dir, 'combined_report.xlsx')
|
128 |
-
|
129 |
-
# تصدير البيانات متعددة الصفحات
|
130 |
-
result1 = export_to_excel(self.projects_data, output_file, sheet_name='المشاريع', title='تقرير المشاريع')
|
131 |
-
result2 = export_to_excel(self.pricing_data, output_file, sheet_name='التسعير', title='تقرير التسعير', append=True)
|
132 |
-
result3 = export_to_excel(self.risks_data, output_file, sheet_name='المخاطر', title='تقرير المخاطر', append=True)
|
133 |
-
|
134 |
-
# التحقق من نجاح التصدير
|
135 |
-
self.assertTrue(result1)
|
136 |
-
self.assertTrue(result2)
|
137 |
-
self.assertTrue(result3)
|
138 |
-
self.assertTrue(os.path.exists(output_file))
|
139 |
-
|
140 |
-
# التحقق من وجود جميع الصفحات
|
141 |
-
excel_file = pd.ExcelFile(output_file)
|
142 |
-
self.assertIn('المشاريع', excel_file.sheet_names)
|
143 |
-
self.assertIn('التسعير', excel_file.sheet_names)
|
144 |
-
self.assertIn('المخاطر', excel_file.sheet_names)
|
145 |
-
|
146 |
-
# التحقق من محتوى كل صفحة
|
147 |
-
projects_data = pd.read_excel(output_file, sheet_name='المشاريع', skiprows=2)
|
148 |
-
pricing_data = pd.read_excel(output_file, sheet_name='التسعير', skiprows=2)
|
149 |
-
risks_data = pd.read_excel(output_file, sheet_name='المخاطر', skiprows=2)
|
150 |
-
|
151 |
-
self.assertEqual(len(projects_data), len(self.projects_data))
|
152 |
-
self.assertEqual(len(pricing_data), len(self.pricing_data))
|
153 |
-
self.assertEqual(len(risks_data), len(self.risks_data))
|
154 |
-
|
155 |
-
def test_export_with_formatting(self):
|
156 |
-
"""اختبار تصدير تقرير مع التنسيق"""
|
157 |
-
# إنشاء مسار الملف المصدر
|
158 |
-
output_file = os.path.join(self.test_dir, 'formatted_report.xlsx')
|
159 |
-
|
160 |
-
# تصدير البيانات مع تنسيق
|
161 |
-
formatting = {
|
162 |
-
'header_format': {
|
163 |
-
'bold': True,
|
164 |
-
'bg_color': '#4F81BD',
|
165 |
-
'font_color': 'white',
|
166 |
-
'border': 1,
|
167 |
-
'align': 'center'
|
168 |
-
},
|
169 |
-
'row_formats': [
|
170 |
-
{
|
171 |
-
'columns': ['الإجمالي'],
|
172 |
-
'num_format': '#,##0.00 ريال'
|
173 |
-
},
|
174 |
-
{
|
175 |
-
'columns': ['الكمية'],
|
176 |
-
'num_format': '#,##0.00'
|
177 |
-
}
|
178 |
-
],
|
179 |
-
'conditional_formats': [
|
180 |
-
{
|
181 |
-
'column': 'درجة المخاطرة',
|
182 |
-
'criteria': '>',
|
183 |
-
'value': 6,
|
184 |
-
'format': {'bg_color': '#FF9999'}
|
185 |
-
}
|
186 |
-
]
|
187 |
-
}
|
188 |
-
|
189 |
-
result = export_to_excel(self.risks_data, output_file, sheet_name='المخاطر',
|
190 |
-
title='تقرير المخاطر', formatting=formatting)
|
191 |
-
|
192 |
-
# التحقق من نجاح التصدير
|
193 |
-
self.assertTrue(result)
|
194 |
-
self.assertTrue(os.path.exists(output_file))
|
195 |
-
|
196 |
-
def test_export_with_filters(self):
|
197 |
-
"""اختبار تصدير تقرير مع تصفية"""
|
198 |
-
# إنشاء مسار الملف المصدر
|
199 |
-
output_file = os.path.join(self.test_dir, 'filtered_report.xlsx')
|
200 |
-
|
201 |
-
# تصدير البيانات المشاريع المفلترة (فقط مشاريع وزارة الصحة)
|
202 |
-
filtered_data = self.projects_data[self.projects_data['العميل'] == 'وزارة الصحة']
|
203 |
-
result = export_to_excel(filtered_data, output_file, sheet_name='مشاريع الصحة',
|
204 |
-
title='تقرير مشاريع وزارة الصحة')
|
205 |
-
|
206 |
-
# التحقق من نجاح التصدير
|
207 |
-
self.assertTrue(result)
|
208 |
-
self.assertTrue(os.path.exists(output_file))
|
209 |
-
|
210 |
-
# التحقق من محتوى الملف المصدر
|
211 |
-
imported_data = pd.read_excel(output_file, sheet_name='مشاريع الصحة', skiprows=2)
|
212 |
-
|
213 |
-
# التحقق من أن جميع المشاريع تنتمي لوزارة الصحة
|
214 |
-
self.assertEqual(len(imported_data), 2)
|
215 |
-
self.assertTrue(all(client == 'وزارة الصحة' for client in imported_data['العميل']))
|
216 |
-
|
217 |
-
def test_export_with_summary(self):
|
218 |
-
"""اختبار تصدير تقرير مع ملخص"""
|
219 |
-
# إنشاء مسار الملف المصدر
|
220 |
-
output_file = os.path.join(self.test_dir, 'summary_report.xlsx')
|
221 |
-
|
222 |
-
# إنشاء بيانات الملخص
|
223 |
-
summary_data = pd.DataFrame({
|
224 |
-
'المؤشر': ['إجمالي المشاريع', 'المشاريع النشطة', 'المشاريع المرساة', 'نسبة النجاح'],
|
225 |
-
'القيمة': [5, 4, 2, '40%']
|
226 |
-
})
|
227 |
-
|
228 |
-
# تصدير البيانات مع ملخص
|
229 |
-
result1 = export_to_excel(summary_data, output_file, sheet_name='الملخص',
|
230 |
-
title='ملخص تقرير المشاريع')
|
231 |
-
result2 = export_to_excel(self.projects_data, output_file, sheet_name='البيانات',
|
232 |
-
title='تفاصيل المشاريع', append=True)
|
233 |
-
|
234 |
-
# التحقق من نجاح التصدير
|
235 |
-
self.assertTrue(result1)
|
236 |
-
self.assertTrue(result2)
|
237 |
-
self.assertTrue(os.path.exists(output_file))
|
238 |
-
|
239 |
-
# التحقق من وجود صفحات الملخص والبيانات
|
240 |
-
excel_file = pd.ExcelFile(output_file)
|
241 |
-
self.assertIn('الملخص', excel_file.sheet_names)
|
242 |
-
self.assertIn('البيانات', excel_file.sheet_names)
|
243 |
-
|
244 |
-
|
245 |
-
if __name__ == '__main__':
|
246 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_app.py
DELETED
@@ -1,601 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
وحدة اختبار التطبيق لنظام إدارة المناقصات - Hybrid Face
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import sys
|
7 |
-
import logging
|
8 |
-
import unittest
|
9 |
-
import tkinter as tk
|
10 |
-
from pathlib import Path
|
11 |
-
|
12 |
-
# تهيئة السجل
|
13 |
-
logging.basicConfig(
|
14 |
-
level=logging.INFO,
|
15 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
16 |
-
)
|
17 |
-
logger = logging.getLogger('test_app')
|
18 |
-
|
19 |
-
# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
|
20 |
-
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
21 |
-
|
22 |
-
# استيراد الوحدات المطلوبة للاختبار
|
23 |
-
from database.db_connector import DatabaseConnector
|
24 |
-
from database.models import User, Project, Document, ProjectItem, Resource, Risk, Report, SystemLog
|
25 |
-
from modules.document_analysis.analyzer import DocumentAnalyzer
|
26 |
-
from modules.pricing.pricing_engine import PricingEngine
|
27 |
-
from modules.risk_analysis.risk_analyzer import RiskAnalyzer
|
28 |
-
from modules.ai_assistant.assistant import AIAssistant
|
29 |
-
from styling.theme import AppTheme
|
30 |
-
from styling.icons import IconGenerator
|
31 |
-
from styling.charts import ChartGenerator
|
32 |
-
from config import AppConfig
|
33 |
-
|
34 |
-
class TestDatabaseConnector(unittest.TestCase):
|
35 |
-
"""اختبار وحدة اتصال قاعدة البيانات"""
|
36 |
-
|
37 |
-
def setUp(self):
|
38 |
-
"""إعداد بيئة الاختبار"""
|
39 |
-
# استخدام قاعدة بيانات اختبار مؤقتة
|
40 |
-
self.config = AppConfig()
|
41 |
-
self.config.DB_NAME = "test_tender_system.db"
|
42 |
-
self.db = DatabaseConnector(self.config)
|
43 |
-
|
44 |
-
def tearDown(self):
|
45 |
-
"""تنظيف بيئة الاختبار"""
|
46 |
-
self.db.disconnect()
|
47 |
-
# حذف قاعدة البيانات المؤقتة
|
48 |
-
if os.path.exists(self.config.DB_NAME):
|
49 |
-
os.remove(self.config.DB_NAME)
|
50 |
-
|
51 |
-
def test_connection(self):
|
52 |
-
"""اختبار الاتصال بقاعدة البيانات"""
|
53 |
-
self.assertTrue(self.db.is_connected)
|
54 |
-
|
55 |
-
def test_execute_query(self):
|
56 |
-
"""اختبار تنفيذ استعلام"""
|
57 |
-
cursor = self.db.execute("SELECT 1")
|
58 |
-
self.assertIsNotNone(cursor)
|
59 |
-
result = cursor.fetchone()
|
60 |
-
self.assertEqual(result[0], 1)
|
61 |
-
|
62 |
-
def test_insert_and_fetch(self):
|
63 |
-
"""اختبار إدراج واسترجاع البيانات"""
|
64 |
-
# إدراج بيانات
|
65 |
-
user_data = {
|
66 |
-
"username": "test_user",
|
67 |
-
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
68 |
-
"full_name": "مستخدم اختبار",
|
69 |
-
"email": "[email protected]",
|
70 |
-
"role": "user"
|
71 |
-
}
|
72 |
-
user_id = self.db.insert("users", user_data)
|
73 |
-
self.assertIsNotNone(user_id)
|
74 |
-
|
75 |
-
# استرجاع البيانات
|
76 |
-
user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
77 |
-
self.assertIsNotNone(user)
|
78 |
-
self.assertEqual(user["username"], "test_user")
|
79 |
-
self.assertEqual(user["full_name"], "مستخدم اختبار")
|
80 |
-
|
81 |
-
def test_update(self):
|
82 |
-
"""اختبار تحديث البيانات"""
|
83 |
-
# إدراج بيانات
|
84 |
-
user_data = {
|
85 |
-
"username": "update_user",
|
86 |
-
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
87 |
-
"full_name": "مستخدم للتحديث",
|
88 |
-
"email": "[email protected]",
|
89 |
-
"role": "user"
|
90 |
-
}
|
91 |
-
user_id = self.db.insert("users", user_data)
|
92 |
-
|
93 |
-
# تحديث البيانات
|
94 |
-
updated_data = {
|
95 |
-
"full_name": "مستخدم تم تحديثه",
|
96 |
-
"role": "admin"
|
97 |
-
}
|
98 |
-
rows_affected = self.db.update("users", updated_data, "id = ?", (user_id,))
|
99 |
-
self.assertEqual(rows_affected, 1)
|
100 |
-
|
101 |
-
# التحقق من التحديث
|
102 |
-
user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
103 |
-
self.assertEqual(user["full_name"], "مستخدم تم تحديثه")
|
104 |
-
self.assertEqual(user["role"], "admin")
|
105 |
-
|
106 |
-
def test_delete(self):
|
107 |
-
"""اختبار حذف البيانات"""
|
108 |
-
# إدراج بيانات
|
109 |
-
user_data = {
|
110 |
-
"username": "delete_user",
|
111 |
-
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
112 |
-
"full_name": "مستخدم للحذف",
|
113 |
-
"email": "[email protected]",
|
114 |
-
"role": "user"
|
115 |
-
}
|
116 |
-
user_id = self.db.insert("users", user_data)
|
117 |
-
|
118 |
-
# حذف البيانات
|
119 |
-
rows_affected = self.db.delete("users", "id = ?", (user_id,))
|
120 |
-
self.assertEqual(rows_affected, 1)
|
121 |
-
|
122 |
-
# التحقق من الحذف
|
123 |
-
user = self.db.fetch_one("SELECT * FROM users WHERE id = ?", (user_id,))
|
124 |
-
self.assertIsNone(user)
|
125 |
-
|
126 |
-
|
127 |
-
class TestModels(unittest.TestCase):
|
128 |
-
"""اختبار نماذج البيانات"""
|
129 |
-
|
130 |
-
def setUp(self):
|
131 |
-
"""��عداد بيئة الاختبار"""
|
132 |
-
# استخدام قاعدة بيانات اختبار مؤقتة
|
133 |
-
self.config = AppConfig()
|
134 |
-
self.config.DB_NAME = "test_models.db"
|
135 |
-
self.db = DatabaseConnector(self.config)
|
136 |
-
|
137 |
-
def tearDown(self):
|
138 |
-
"""تنظيف بيئة الاختبار"""
|
139 |
-
self.db.disconnect()
|
140 |
-
# حذف قاعدة البيانات المؤقتة
|
141 |
-
if os.path.exists(self.config.DB_NAME):
|
142 |
-
os.remove(self.config.DB_NAME)
|
143 |
-
|
144 |
-
def test_user_model(self):
|
145 |
-
"""اختبار نموذج المستخدم"""
|
146 |
-
# إنشاء مستخدم جديد
|
147 |
-
user = User(self.db)
|
148 |
-
user.data = {
|
149 |
-
"username": "model_user",
|
150 |
-
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
151 |
-
"full_name": "مستخدم نموذج",
|
152 |
-
"email": "[email protected]",
|
153 |
-
"role": "user",
|
154 |
-
"is_active": 1
|
155 |
-
}
|
156 |
-
|
157 |
-
# حفظ المستخدم
|
158 |
-
self.assertTrue(user.save())
|
159 |
-
self.assertIsNotNone(user.data.get("id"))
|
160 |
-
|
161 |
-
# استرجاع المستخدم
|
162 |
-
retrieved_user = User.get_by_id(user.data["id"], self.db)
|
163 |
-
self.assertIsNotNone(retrieved_user)
|
164 |
-
self.assertEqual(retrieved_user.data["username"], "model_user")
|
165 |
-
|
166 |
-
# مصادقة المستخدم
|
167 |
-
authenticated_user = User.authenticate("model_user", "admin", self.db)
|
168 |
-
self.assertIsNotNone(authenticated_user)
|
169 |
-
self.assertEqual(authenticated_user.data["username"], "model_user")
|
170 |
-
|
171 |
-
# تعيين كلمة مرور جديدة
|
172 |
-
user.set_password("newpassword")
|
173 |
-
user.save()
|
174 |
-
|
175 |
-
# مصادقة المستخدم بكلمة المرور الجديدة
|
176 |
-
authenticated_user = User.authenticate("model_user", "newpassword", self.db)
|
177 |
-
self.assertIsNotNone(authenticated_user)
|
178 |
-
|
179 |
-
# حذف المستخدم
|
180 |
-
self.assertTrue(user.delete())
|
181 |
-
|
182 |
-
def test_project_model(self):
|
183 |
-
"""اختبار نموذج المشروع"""
|
184 |
-
# إنشاء مستخدم للمشروع
|
185 |
-
user = User(self.db)
|
186 |
-
user.data = {
|
187 |
-
"username": "project_user",
|
188 |
-
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", # admin
|
189 |
-
"full_name": "مستخدم المشروع",
|
190 |
-
"email": "[email protected]",
|
191 |
-
"role": "user",
|
192 |
-
"is_active": 1
|
193 |
-
}
|
194 |
-
user.save()
|
195 |
-
|
196 |
-
# إنشاء مشروع جديد
|
197 |
-
project = Project(self.db)
|
198 |
-
project.data = {
|
199 |
-
"name": "مشروع اختبار",
|
200 |
-
"client": "عميل اختبار",
|
201 |
-
"description": "وصف مشروع الاختبار",
|
202 |
-
"start_date": "2025-01-01",
|
203 |
-
"end_date": "2025-12-31",
|
204 |
-
"status": "تخطيط",
|
205 |
-
"created_by": user.data["id"]
|
206 |
-
}
|
207 |
-
|
208 |
-
# حفظ المشروع
|
209 |
-
self.assertTrue(project.save())
|
210 |
-
self.assertIsNotNone(project.data.get("id"))
|
211 |
-
|
212 |
-
# استرجاع المشروع
|
213 |
-
retrieved_project = Project.get_by_id(project.data["id"], self.db)
|
214 |
-
self.assertIsNotNone(retrieved_project)
|
215 |
-
self.assertEqual(retrieved_project.data["name"], "مشروع اختبار")
|
216 |
-
|
217 |
-
# إضافة بند للمشروع
|
218 |
-
item = ProjectItem(self.db)
|
219 |
-
item.data = {
|
220 |
-
"project_id": project.data["id"],
|
221 |
-
"name": "بند اختبار",
|
222 |
-
"description": "وصف بند الاختبار",
|
223 |
-
"unit": "م²",
|
224 |
-
"quantity": 100,
|
225 |
-
"unit_price": 500,
|
226 |
-
"total_price": 50000
|
227 |
-
}
|
228 |
-
item.save()
|
229 |
-
|
230 |
-
# حساب التكلفة الإجمالية للمشروع
|
231 |
-
total_cost = project.calculate_total_cost()
|
232 |
-
self.assertEqual(total_cost, 50000)
|
233 |
-
|
234 |
-
# حذف المشروع
|
235 |
-
self.assertTrue(project.delete())
|
236 |
-
|
237 |
-
# حذف المستخدم
|
238 |
-
self.assertTrue(user.delete())
|
239 |
-
|
240 |
-
|
241 |
-
class TestDocumentAnalyzer(unittest.TestCase):
|
242 |
-
"""اختبار محلل المستندات"""
|
243 |
-
|
244 |
-
def setUp(self):
|
245 |
-
"""إعداد بيئة الاختبار"""
|
246 |
-
self.config = AppConfig()
|
247 |
-
self.analyzer = DocumentAnalyzer(self.config)
|
248 |
-
|
249 |
-
# إنشاء مجلد المستندات للاختبار
|
250 |
-
self.test_docs_dir = Path("test_documents")
|
251 |
-
self.test_docs_dir.mkdir(exist_ok=True)
|
252 |
-
|
253 |
-
# إنشاء ملف مستند اختبار
|
254 |
-
self.test_doc_path = self.test_docs_dir / "test_document.txt"
|
255 |
-
with open(self.test_doc_path, "w", encoding="utf-8") as f:
|
256 |
-
f.write("هذا مستند اختبار لمحلل المستندات")
|
257 |
-
|
258 |
-
def tearDown(self):
|
259 |
-
"""تنظيف بيئة الاختبار"""
|
260 |
-
# حذف ملف المستند
|
261 |
-
if self.test_doc_path.exists():
|
262 |
-
self.test_doc_path.unlink()
|
263 |
-
|
264 |
-
# حذف مجلد المستندات
|
265 |
-
if self.test_docs_dir.exists():
|
266 |
-
self.test_docs_dir.rmdir()
|
267 |
-
|
268 |
-
def test_analyze_document(self):
|
269 |
-
"""اختبار تحليل المستند"""
|
270 |
-
# تحليل المستند
|
271 |
-
result = self.analyzer.analyze_document(str(self.test_doc_path), "tender")
|
272 |
-
self.assertTrue(result)
|
273 |
-
|
274 |
-
# انتظار اكتمال التحليل
|
275 |
-
import time
|
276 |
-
max_wait = 5 # ثوانٍ
|
277 |
-
waited = 0
|
278 |
-
while self.analyzer.analysis_in_progress and waited < max_wait:
|
279 |
-
time.sleep(0.5)
|
280 |
-
waited += 0.5
|
281 |
-
|
282 |
-
# التحقق من نتائج التحليل
|
283 |
-
self.assertFalse(self.analyzer.analysis_in_progress)
|
284 |
-
results = self.analyzer.get_analysis_results()
|
285 |
-
self.assertEqual(results["status"], "اكتمل التحليل")
|
286 |
-
self.assertEqual(results["document_path"], str(self.test_doc_path))
|
287 |
-
|
288 |
-
def test_export_analysis_results(self):
|
289 |
-
"""اختبار تصدير نتائج التحليل"""
|
290 |
-
# تحليل المستند
|
291 |
-
self.analyzer.analyze_document(str(self.test_doc_path), "tender")
|
292 |
-
|
293 |
-
# انتظار اكتمال التحليل
|
294 |
-
import time
|
295 |
-
max_wait = 5 # ثوانٍ
|
296 |
-
waited = 0
|
297 |
-
while self.analyzer.analysis_in_progress and waited < max_wait:
|
298 |
-
time.sleep(0.5)
|
299 |
-
waited += 0.5
|
300 |
-
|
301 |
-
# تصدير النتائج
|
302 |
-
export_path = self.test_docs_dir / "analysis_results.json"
|
303 |
-
result_path = self.analyzer.export_analysis_results(str(export_path))
|
304 |
-
self.assertIsNotNone(result_path)
|
305 |
-
|
306 |
-
# التحقق من وجود ملف التصدير
|
307 |
-
self.assertTrue(export_path.exists())
|
308 |
-
|
309 |
-
# حذف ملف التصدير
|
310 |
-
if export_path.exists():
|
311 |
-
export_path.unlink()
|
312 |
-
|
313 |
-
|
314 |
-
class TestPricingEngine(unittest.TestCase):
|
315 |
-
"""اختبار محرك التسعير"""
|
316 |
-
|
317 |
-
def setUp(self):
|
318 |
-
"""إعداد بيئة الاختبار"""
|
319 |
-
self.config = AppConfig()
|
320 |
-
self.pricing_engine = PricingEngine(self.config)
|
321 |
-
|
322 |
-
def test_calculate_pricing(self):
|
323 |
-
"""اختبار حساب التسعير"""
|
324 |
-
# حساب التسعير
|
325 |
-
result = self.pricing_engine.calculate_pricing(1, "comprehensive")
|
326 |
-
self.assertTrue(result)
|
327 |
-
|
328 |
-
# انتظار اكتمال التسعير
|
329 |
-
import time
|
330 |
-
max_wait = 5 # ثوانٍ
|
331 |
-
waited = 0
|
332 |
-
while self.pricing_engine.pricing_in_progress and waited < max_wait:
|
333 |
-
time.sleep(0.5)
|
334 |
-
waited += 0.5
|
335 |
-
|
336 |
-
# التحقق من نتائج التسعير
|
337 |
-
self.assertFalse(self.pricing_engine.pricing_in_progress)
|
338 |
-
results = self.pricing_engine.get_pricing_results()
|
339 |
-
self.assertEqual(results["status"], "اكتمل التسعير")
|
340 |
-
self.assertEqual(results["project_id"], 1)
|
341 |
-
self.assertEqual(results["strategy"], "comprehensive")
|
342 |
-
|
343 |
-
# التحقق من وجود التكاليف المباشرة
|
344 |
-
self.assertIn("direct_costs", results)
|
345 |
-
self.assertIn("total_direct_costs", results["direct_costs"])
|
346 |
-
|
347 |
-
# التحقق من وجود التكاليف غير المباشرة
|
348 |
-
self.assertIn("indirect_costs", results)
|
349 |
-
self.assertIn("total_indirect_costs", results["indirect_costs"])
|
350 |
-
|
351 |
-
# التحقق من وجود تكاليف المخاطر
|
352 |
-
self.assertIn("risk_costs", results)
|
353 |
-
self.assertIn("total_risk_cost", results["risk_costs"])
|
354 |
-
|
355 |
-
# التحقق من وجود ملخص التسعير
|
356 |
-
self.assertIn("summary", results)
|
357 |
-
self.assertIn("final_price", results["summary"])
|
358 |
-
|
359 |
-
|
360 |
-
class TestRiskAnalyzer(unittest.TestCase):
|
361 |
-
"""اختبار محلل المخاطر"""
|
362 |
-
|
363 |
-
def setUp(self):
|
364 |
-
"""إعداد بيئة الاختبار"""
|
365 |
-
self.config = AppConfig()
|
366 |
-
self.risk_analyzer = RiskAnalyzer(self.config)
|
367 |
-
|
368 |
-
def test_analyze_risks(self):
|
369 |
-
"""اختبار تحليل المخاطر"""
|
370 |
-
# تحليل المخاطر
|
371 |
-
result = self.risk_analyzer.analyze_risks(1, "comprehensive")
|
372 |
-
self.assertTrue(result)
|
373 |
-
|
374 |
-
# انتظار اكتمال التحليل
|
375 |
-
import time
|
376 |
-
max_wait = 5 # ثوانٍ
|
377 |
-
waited = 0
|
378 |
-
while self.risk_analyzer.analysis_in_progress and waited < max_wait:
|
379 |
-
time.sleep(0.5)
|
380 |
-
waited += 0.5
|
381 |
-
|
382 |
-
# التحقق من نتائج التحليل
|
383 |
-
self.assertFalse(self.risk_analyzer.analysis_in_progress)
|
384 |
-
results = self.risk_analyzer.get_analysis_results()
|
385 |
-
self.assertEqual(results["status"], "اكتمل التحليل")
|
386 |
-
self.assertEqual(results["project_id"], 1)
|
387 |
-
self.assertEqual(results["method"], "comprehensive")
|
388 |
-
|
389 |
-
# التحقق من وجود المخاطر المحددة
|
390 |
-
self.assertIn("identified_risks", results)
|
391 |
-
self.assertTrue(len(results["identified_risks"]) > 0)
|
392 |
-
|
393 |
-
# التحقق من وجود فئات المخاطر
|
394 |
-
self.assertIn("risk_categories", results)
|
395 |
-
|
396 |
-
# التحقق من وجود مصفوفة المخاطر
|
397 |
-
self.assertIn("risk_matrix", results)
|
398 |
-
|
399 |
-
# التحقق من وجود استراتيجيات التخفيف
|
400 |
-
self.assertIn("mitigation_strategies", results)
|
401 |
-
self.assertTrue(len(results["mitigation_strategies"]) > 0)
|
402 |
-
|
403 |
-
# التحقق من وجود ملخص التحليل
|
404 |
-
self.assertIn("summary", results)
|
405 |
-
self.assertIn("overall_risk_level", results["summary"])
|
406 |
-
|
407 |
-
|
408 |
-
class TestAIAssistant(unittest.TestCase):
|
409 |
-
"""اختبار المساعد الذكي"""
|
410 |
-
|
411 |
-
def setUp(self):
|
412 |
-
"""إعداد بيئة الاختبار"""
|
413 |
-
self.config = AppConfig()
|
414 |
-
self.assistant = AIAssistant(self.config)
|
415 |
-
|
416 |
-
def test_process_query(self):
|
417 |
-
"""اختبار معالجة الاستعلام"""
|
418 |
-
# معالجة استعلام
|
419 |
-
query = "كيف يمكنني تحليل مستند مناقصة؟"
|
420 |
-
result = self.assistant.process_query(query)
|
421 |
-
self.assertTrue(result)
|
422 |
-
|
423 |
-
# انتظار اكتمال المعالجة
|
424 |
-
import time
|
425 |
-
max_wait = 5 # ثوانٍ
|
426 |
-
waited = 0
|
427 |
-
while self.assistant.processing_in_progress and waited < max_wait:
|
428 |
-
time.sleep(0.5)
|
429 |
-
waited += 0.5
|
430 |
-
|
431 |
-
# التحقق من نتائج المعالجة
|
432 |
-
self.assertFalse(self.assistant.processing_in_progress)
|
433 |
-
results = self.assistant.get_processing_results()
|
434 |
-
self.assertEqual(results["status"], "اكتملت المعالجة")
|
435 |
-
self.assertEqual(results["query"], query)
|
436 |
-
|
437 |
-
# التحقق من وجود استجابة
|
438 |
-
self.assertIn("response", results)
|
439 |
-
self.assertTrue(len(results["response"]) > 0)
|
440 |
-
|
441 |
-
# التحقق من وجود اقتراحات
|
442 |
-
self.assertIn("suggestions", results)
|
443 |
-
self.assertTrue(len(results["suggestions"]) > 0)
|
444 |
-
|
445 |
-
def test_conversation_history(self):
|
446 |
-
"""اختبار سجل المحادثة"""
|
447 |
-
# معالجة استعلام
|
448 |
-
query = "ما هي استراتيجيات التسعير المتاحة؟"
|
449 |
-
self.assistant.process_query(query)
|
450 |
-
|
451 |
-
# انتظار اكتمال المعالجة
|
452 |
-
import time
|
453 |
-
max_wait = 5 # ثوانٍ
|
454 |
-
waited = 0
|
455 |
-
while self.assistant.processing_in_progress and waited < max_wait:
|
456 |
-
time.sleep(0.5)
|
457 |
-
waited += 0.5
|
458 |
-
|
459 |
-
# التحقق من سجل المحادثة
|
460 |
-
history = self.assistant.get_conversation_history()
|
461 |
-
self.assertEqual(len(history), 2) # استعلام المستخدم واستجابة المساعد
|
462 |
-
self.assertEqual(history[0]["role"], "user")
|
463 |
-
self.assertEqual(history[0]["content"], query)
|
464 |
-
self.assertEqual(history[1]["role"], "assistant")
|
465 |
-
|
466 |
-
# مسح سجل المحادثة
|
467 |
-
self.assertTrue(self.assistant.clear_conversation_history())
|
468 |
-
history = self.assistant.get_conversation_history()
|
469 |
-
self.assertEqual(len(history), 0)
|
470 |
-
|
471 |
-
|
472 |
-
class TestStyling(unittest.TestCase):
|
473 |
-
"""اختبار وحدات التصميم"""
|
474 |
-
|
475 |
-
def test_app_theme(self):
|
476 |
-
"""اختبار نمط التطبيق"""
|
477 |
-
theme = AppTheme()
|
478 |
-
|
479 |
-
# التحقق من الألوان
|
480 |
-
self.assertIsNotNone(theme.get_color("bg_color"))
|
481 |
-
self.assertIsNotNone(theme.get_color("fg_color"))
|
482 |
-
|
483 |
-
# التحقق من الخطوط
|
484 |
-
self.assertIsNotNone(theme.get_font("body"))
|
485 |
-
self.assertIsNotNone(theme.get_font("title"))
|
486 |
-
|
487 |
-
# التحقق من الأحجام
|
488 |
-
self.assertIsNotNone(theme.get_size("padding_medium"))
|
489 |
-
self.assertIsNotNone(theme.get_size("border_radius"))
|
490 |
-
|
491 |
-
# تغيير النمط
|
492 |
-
self.assertTrue(theme.set_theme("dark"))
|
493 |
-
self.assertEqual(theme.current_theme, "dark")
|
494 |
-
|
495 |
-
# تغيير اللغة
|
496 |
-
self.assertTrue(theme.set_language("en"))
|
497 |
-
self.assertEqual(theme.current_language, "en")
|
498 |
-
|
499 |
-
def test_icon_generator(self):
|
500 |
-
"""اختبار مولد الأيقونات"""
|
501 |
-
icon_generator = IconGenerator()
|
502 |
-
|
503 |
-
# توليد الأيقونات الافتراضية
|
504 |
-
icon_generator.generate_default_icons()
|
505 |
-
|
506 |
-
# التحقق من وجود مجلد الأيقونات
|
507 |
-
self.assertTrue(Path('assets/icons').exists())
|
508 |
-
|
509 |
-
# التحقق من وجود بعض الأيقونات
|
510 |
-
self.assertTrue(Path('assets/icons/dashboard.png').exists())
|
511 |
-
self.assertTrue(Path('assets/icons/projects.png').exists())
|
512 |
-
|
513 |
-
def test_chart_generator(self):
|
514 |
-
"""اختبار مولد الرسوم البيانية"""
|
515 |
-
theme = AppTheme()
|
516 |
-
chart_generator = ChartGenerator(theme)
|
517 |
-
|
518 |
-
# إنشاء بيانات للرسم البياني الشريطي
|
519 |
-
bar_data = {
|
520 |
-
'labels': ['الربع الأول', 'الربع الثاني', 'الربع الثالث', 'الربع الرابع'],
|
521 |
-
'values': [15000, 20000, 18000, 25000]
|
522 |
-
}
|
523 |
-
|
524 |
-
# إنشاء رسم بياني شريطي
|
525 |
-
fig = chart_generator.create_bar_chart(
|
526 |
-
bar_data,
|
527 |
-
'الإيرادات الفصلية',
|
528 |
-
'الفصل',
|
529 |
-
'الإيرادات (ريال)'
|
530 |
-
)
|
531 |
-
|
532 |
-
# التحقق من إنشاء الرسم البياني
|
533 |
-
self.assertIsNotNone(fig)
|
534 |
-
|
535 |
-
# حفظ الرسم البياني
|
536 |
-
save_path = 'test_chart.png'
|
537 |
-
chart_generator.create_bar_chart(
|
538 |
-
bar_data,
|
539 |
-
'الإيرادات الفصلية',
|
540 |
-
'الفصل',
|
541 |
-
'الإيرادات (ريال)',
|
542 |
-
save_path=save_path
|
543 |
-
)
|
544 |
-
|
545 |
-
# التحقق من وجود ملف الرسم البياني
|
546 |
-
self.assertTrue(Path(save_path).exists())
|
547 |
-
|
548 |
-
# حذف ملف الرسم البياني
|
549 |
-
if Path(save_path).exists():
|
550 |
-
Path(save_path).unlink()
|
551 |
-
|
552 |
-
|
553 |
-
def run_tests():
|
554 |
-
"""تشغيل الاختبارات"""
|
555 |
-
# إنشاء مجلد الاختبارات
|
556 |
-
test_dir = Path('test_results')
|
557 |
-
test_dir.mkdir(exist_ok=True)
|
558 |
-
|
559 |
-
# إنشاء ملف لنتائج الاختبارات
|
560 |
-
test_results_file = test_dir / 'test_results.txt'
|
561 |
-
|
562 |
-
# تشغيل الاختبارات وحفظ النتائج
|
563 |
-
with open(test_results_file, 'w', encoding='utf-8') as f:
|
564 |
-
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
565 |
-
suite = unittest.TestSuite()
|
566 |
-
|
567 |
-
# إضافة اختبارات قاعدة البيانات
|
568 |
-
suite.addTest(unittest.makeSuite(TestDatabaseConnector))
|
569 |
-
suite.addTest(unittest.makeSuite(TestModels))
|
570 |
-
|
571 |
-
# إضافة اختبارات الوحدات
|
572 |
-
suite.addTest(unittest.makeSuite(TestDocumentAnalyzer))
|
573 |
-
suite.addTest(unittest.makeSuite(TestPricingEngine))
|
574 |
-
suite.addTest(unittest.makeSuite(TestRiskAnalyzer))
|
575 |
-
suite.addTest(unittest.makeSuite(TestAIAssistant))
|
576 |
-
|
577 |
-
# إضافة اختبارات التصميم
|
578 |
-
suite.addTest(unittest.makeSuite(TestStyling))
|
579 |
-
|
580 |
-
# تشغيل الاختبارات
|
581 |
-
result = runner.run(suite)
|
582 |
-
|
583 |
-
# كتابة ملخص النتائج
|
584 |
-
f.write("\n\n=== ملخص نتائج الاختبارات ===\n")
|
585 |
-
f.write(f"عدد الاختبارات: {result.testsRun}\n")
|
586 |
-
f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
|
587 |
-
f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
|
588 |
-
f.write(f"عدد الأخطاء: {len(result.errors)}\n")
|
589 |
-
|
590 |
-
# طباعة ملخص النتائج
|
591 |
-
logger.info(f"تم تشغيل {result.testsRun} اختبار")
|
592 |
-
logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
|
593 |
-
logger.info(f"الإخفاقات: {len(result.failures)}")
|
594 |
-
logger.info(f"الأخطاء: {len(result.errors)}")
|
595 |
-
logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
|
596 |
-
|
597 |
-
return result
|
598 |
-
|
599 |
-
|
600 |
-
if __name__ == "__main__":
|
601 |
-
run_tests()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_integrated_system.py
DELETED
@@ -1,96 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
وحدة اختبار النظام المتكامل
|
3 |
-
"""
|
4 |
-
|
5 |
-
import unittest
|
6 |
-
import os
|
7 |
-
import sys
|
8 |
-
from pathlib import Path
|
9 |
-
|
10 |
-
# إضافة المسار الرئيسي للنظام
|
11 |
-
sys.path.append(str(Path(__file__).parent.parent))
|
12 |
-
|
13 |
-
# استيراد الوحدات
|
14 |
-
from modules.pricing.pricing_app import PricingApp
|
15 |
-
from modules.ai_assistant.ai_app import AIAssistantApp
|
16 |
-
from modules.document_analysis.document_app import DocumentAnalysisApp
|
17 |
-
from modules.data_analysis.data_analysis_app import DataAnalysisApp
|
18 |
-
from modules.resources.resources_app import ResourcesApp
|
19 |
-
|
20 |
-
class TestIntegratedSystem(unittest.TestCase):
|
21 |
-
"""اختبارات النظام المتكامل"""
|
22 |
-
|
23 |
-
def setUp(self):
|
24 |
-
"""إعداد بيئة الاختبار"""
|
25 |
-
# التأكد من وجود جميع الملفات الرئيسية
|
26 |
-
self.main_files = [
|
27 |
-
"app.py",
|
28 |
-
"config.py",
|
29 |
-
"requirements.txt"
|
30 |
-
]
|
31 |
-
|
32 |
-
# التأكد من وجود جميع المجلدات الرئيسية
|
33 |
-
self.main_directories = [
|
34 |
-
"modules",
|
35 |
-
"assets",
|
36 |
-
"data",
|
37 |
-
"utils"
|
38 |
-
]
|
39 |
-
|
40 |
-
# التأكد من وجود جميع وحدات النظام
|
41 |
-
self.modules = [
|
42 |
-
"modules/pricing",
|
43 |
-
"modules/ai_assistant",
|
44 |
-
"modules/document_analysis",
|
45 |
-
"modules/data_analysis",
|
46 |
-
"modules/resources",
|
47 |
-
"modules/project_management",
|
48 |
-
"modules/reports",
|
49 |
-
"modules/risk_analysis"
|
50 |
-
]
|
51 |
-
|
52 |
-
def test_main_files_exist(self):
|
53 |
-
"""اختبار وجود الملفات الرئيسية"""
|
54 |
-
for file in self.main_files:
|
55 |
-
file_path = Path(__file__).parent.parent / file
|
56 |
-
self.assertTrue(file_path.exists(), f"الملف {file} غير موجود")
|
57 |
-
|
58 |
-
def test_main_directories_exist(self):
|
59 |
-
"""اختبار وجود المجلدات الرئيسية"""
|
60 |
-
for directory in self.main_directories:
|
61 |
-
dir_path = Path(__file__).parent.parent / directory
|
62 |
-
self.assertTrue(dir_path.exists(), f"المجلد {directory} غير موجود")
|
63 |
-
|
64 |
-
def test_modules_exist(self):
|
65 |
-
"""اختبار وجود وحدات النظام"""
|
66 |
-
for module in self.modules:
|
67 |
-
module_path = Path(__file__).parent.parent / module
|
68 |
-
self.assertTrue(module_path.exists(), f"الوحدة {module} غير موجودة")
|
69 |
-
|
70 |
-
def test_pricing_module(self):
|
71 |
-
"""اختبار وحدة التسعير"""
|
72 |
-
pricing_app = PricingApp()
|
73 |
-
self.assertIsNotNone(pricing_app, "فشل إنشاء وحدة التسعير")
|
74 |
-
|
75 |
-
def test_ai_assistant_module(self):
|
76 |
-
"""اختبار وحدة الذكاء الاصطناعي"""
|
77 |
-
ai_app = AIAssistantApp()
|
78 |
-
self.assertIsNotNone(ai_app, "فشل إنشاء وحدة الذكاء الاصطناعي")
|
79 |
-
|
80 |
-
def test_document_analysis_module(self):
|
81 |
-
"""اختبار وحدة تحليل المستندات"""
|
82 |
-
document_app = DocumentAnalysisApp()
|
83 |
-
self.assertIsNotNone(document_app, "فشل إنشاء وحدة تحليل المستندات")
|
84 |
-
|
85 |
-
def test_data_analysis_module(self):
|
86 |
-
"""اختبار وحدة تحليل البيانات"""
|
87 |
-
data_analysis_app = DataAnalysisApp()
|
88 |
-
self.assertIsNotNone(data_analysis_app, "فشل إنشاء وحدة تحليل البيانات")
|
89 |
-
|
90 |
-
def test_resources_module(self):
|
91 |
-
"""اختبار وحدة الموارد"""
|
92 |
-
resources_app = ResourcesApp()
|
93 |
-
self.assertIsNotNone(resources_app, "فشل إنشاء وحدة الموارد")
|
94 |
-
|
95 |
-
if __name__ == "__main__":
|
96 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_ui.py
DELETED
@@ -1,413 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
وحدة اختبار واجهة المستخدم لنظام إدارة المناقصات - Hybrid Face
|
3 |
-
"""
|
4 |
-
|
5 |
-
import os
|
6 |
-
import sys
|
7 |
-
import logging
|
8 |
-
import unittest
|
9 |
-
import tkinter as tk
|
10 |
-
import customtkinter as ctk
|
11 |
-
from pathlib import Path
|
12 |
-
|
13 |
-
# تهيئة السجل
|
14 |
-
logging.basicConfig(
|
15 |
-
level=logging.INFO,
|
16 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
17 |
-
)
|
18 |
-
logger = logging.getLogger('test_ui')
|
19 |
-
|
20 |
-
# إضافة المسار الرئيسي للتطبيق إلى مسار البحث
|
21 |
-
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
22 |
-
|
23 |
-
# استيراد الوحدات المطلوبة للاختبار
|
24 |
-
from styling.theme import AppTheme
|
25 |
-
from styling.icons import IconGenerator
|
26 |
-
from styling.charts import ChartGenerator
|
27 |
-
from config import AppConfig
|
28 |
-
|
29 |
-
class TestUIComponents(unittest.TestCase):
|
30 |
-
"""اختبار مكونات واجهة المستخدم"""
|
31 |
-
|
32 |
-
def setUp(self):
|
33 |
-
"""إعداد بيئة الاختبار"""
|
34 |
-
self.root = ctk.CTk()
|
35 |
-
self.root.withdraw() # إخفاء النافذة أثناء الاختبار
|
36 |
-
self.theme = AppTheme()
|
37 |
-
|
38 |
-
def tearDown(self):
|
39 |
-
"""تنظيف بيئة الاختبار"""
|
40 |
-
self.root.destroy()
|
41 |
-
|
42 |
-
def test_styled_frame(self):
|
43 |
-
"""اختبار الإطار المنسق"""
|
44 |
-
frame = self.theme.create_styled_frame(self.root)
|
45 |
-
self.assertIsNotNone(frame)
|
46 |
-
self.assertEqual(frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
47 |
-
self.assertEqual(frame.cget("corner_radius"), self.theme.get_size("border_radius"))
|
48 |
-
|
49 |
-
def test_styled_button(self):
|
50 |
-
"""اختبار الزر المنسق"""
|
51 |
-
button = self.theme.create_styled_button(self.root, "زر اختبار")
|
52 |
-
self.assertIsNotNone(button)
|
53 |
-
self.assertEqual(button.cget("text"), "زر اختبار")
|
54 |
-
self.assertEqual(button.cget("fg_color"), self.theme.get_color("button_bg_color"))
|
55 |
-
self.assertEqual(button.cget("text_color"), self.theme.get_color("button_fg_color"))
|
56 |
-
|
57 |
-
def test_styled_label(self):
|
58 |
-
"""اختبار التسمية المنسقة"""
|
59 |
-
label = self.theme.create_styled_label(self.root, "تسمية اختبار")
|
60 |
-
self.assertIsNotNone(label)
|
61 |
-
self.assertEqual(label.cget("text"), "تسمية اختبار")
|
62 |
-
self.assertEqual(label.cget("text_color"), self.theme.get_color("fg_color"))
|
63 |
-
|
64 |
-
def test_styled_entry(self):
|
65 |
-
"""اختبار حقل الإدخال المنسق"""
|
66 |
-
entry = self.theme.create_styled_entry(self.root, "نص توضيحي")
|
67 |
-
self.assertIsNotNone(entry)
|
68 |
-
self.assertEqual(entry.cget("placeholder_text"), "نص توضيحي")
|
69 |
-
self.assertEqual(entry.cget("fg_color"), self.theme.get_color("input_bg_color"))
|
70 |
-
self.assertEqual(entry.cget("text_color"), self.theme.get_color("input_fg_color"))
|
71 |
-
|
72 |
-
def test_styled_combobox(self):
|
73 |
-
"""اختبار القائمة المنسدلة المنسقة"""
|
74 |
-
values = ["الخيار الأول", "الخيار الثاني", "الخيار الثالث"]
|
75 |
-
combobox = self.theme.create_styled_combobox(self.root, values)
|
76 |
-
self.assertIsNotNone(combobox)
|
77 |
-
self.assertEqual(combobox.cget("values"), values)
|
78 |
-
self.assertEqual(combobox.cget("fg_color"), self.theme.get_color("input_bg_color"))
|
79 |
-
self.assertEqual(combobox.cget("text_color"), self.theme.get_color("input_fg_color"))
|
80 |
-
|
81 |
-
def test_styled_checkbox(self):
|
82 |
-
"""اختبار خانة الاختيار المنسقة"""
|
83 |
-
checkbox = self.theme.create_styled_checkbox(self.root, "خانة اختبار")
|
84 |
-
self.assertIsNotNone(checkbox)
|
85 |
-
self.assertEqual(checkbox.cget("text"), "خانة اختبار")
|
86 |
-
self.assertEqual(checkbox.cget("fg_color"), self.theme.get_color("button_bg_color"))
|
87 |
-
self.assertEqual(checkbox.cget("text_color"), self.theme.get_color("fg_color"))
|
88 |
-
|
89 |
-
def test_styled_radio_button(self):
|
90 |
-
"""اختبار زر الراديو المنسق"""
|
91 |
-
var = ctk.StringVar(value="1")
|
92 |
-
radio_button = self.theme.create_styled_radio_button(self.root, "زر راديو اختبار", var, "1")
|
93 |
-
self.assertIsNotNone(radio_button)
|
94 |
-
self.assertEqual(radio_button.cget("text"), "زر راديو اختبار")
|
95 |
-
self.assertEqual(radio_button.cget("fg_color"), self.theme.get_color("button_bg_color"))
|
96 |
-
self.assertEqual(radio_button.cget("text_color"), self.theme.get_color("fg_color"))
|
97 |
-
|
98 |
-
def test_styled_switch(self):
|
99 |
-
"""اختبار مفتاح التبديل المنسق"""
|
100 |
-
switch = self.theme.create_styled_switch(self.root, "مفتاح اختبار")
|
101 |
-
self.assertIsNotNone(switch)
|
102 |
-
self.assertEqual(switch.cget("text"), "مفتاح اختبار")
|
103 |
-
self.assertEqual(switch.cget("progress_color"), self.theme.get_color("button_bg_color"))
|
104 |
-
self.assertEqual(switch.cget("text_color"), self.theme.get_color("fg_color"))
|
105 |
-
|
106 |
-
def test_styled_slider(self):
|
107 |
-
"""اختبار شريط التمرير المنسق"""
|
108 |
-
slider = self.theme.create_styled_slider(self.root)
|
109 |
-
self.assertIsNotNone(slider)
|
110 |
-
self.assertEqual(slider.cget("fg_color"), self.theme.get_color("input_border_color"))
|
111 |
-
self.assertEqual(slider.cget("progress_color"), self.theme.get_color("button_bg_color"))
|
112 |
-
|
113 |
-
def test_styled_progressbar(self):
|
114 |
-
"""اختبار شريط التقدم المنسق"""
|
115 |
-
progressbar = self.theme.create_styled_progressbar(self.root)
|
116 |
-
self.assertIsNotNone(progressbar)
|
117 |
-
self.assertEqual(progressbar.cget("fg_color"), self.theme.get_color("input_border_color"))
|
118 |
-
self.assertEqual(progressbar.cget("progress_color"), self.theme.get_color("button_bg_color"))
|
119 |
-
|
120 |
-
def test_styled_tabview(self):
|
121 |
-
"""اختبار عرض التبويب المنسق"""
|
122 |
-
tabview = self.theme.create_styled_tabview(self.root)
|
123 |
-
self.assertIsNotNone(tabview)
|
124 |
-
self.assertEqual(tabview.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
125 |
-
|
126 |
-
def test_styled_scrollable_frame(self):
|
127 |
-
"""اختبار الإطار القابل للتمرير المنسق"""
|
128 |
-
scrollable_frame = self.theme.create_styled_scrollable_frame(self.root)
|
129 |
-
self.assertIsNotNone(scrollable_frame)
|
130 |
-
self.assertEqual(scrollable_frame.cget("fg_color"), "transparent")
|
131 |
-
|
132 |
-
def test_styled_textbox(self):
|
133 |
-
"""اختبار مربع النص المنسق"""
|
134 |
-
textbox = self.theme.create_styled_textbox(self.root)
|
135 |
-
self.assertIsNotNone(textbox)
|
136 |
-
self.assertEqual(textbox.cget("fg_color"), self.theme.get_color("input_bg_color"))
|
137 |
-
self.assertEqual(textbox.cget("text_color"), self.theme.get_color("input_fg_color"))
|
138 |
-
|
139 |
-
def test_styled_card(self):
|
140 |
-
"""اختبار البطاقة المنسقة"""
|
141 |
-
card, content_frame = self.theme.create_styled_card(self.root, "بطاقة اختبار")
|
142 |
-
self.assertIsNotNone(card)
|
143 |
-
self.assertIsNotNone(content_frame)
|
144 |
-
self.assertEqual(card.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
145 |
-
|
146 |
-
def test_styled_data_table(self):
|
147 |
-
"""اختبار جدول البيانات المنسق"""
|
148 |
-
columns = ["العمود الأول", "العمود الثاني", "العمود الثالث"]
|
149 |
-
data = [
|
150 |
-
["بيانات 1-1", "بيانات 1-2", "بيانات 1-3"],
|
151 |
-
["بيانات 2-1", "بيانات 2-2", "بيانات 2-3"]
|
152 |
-
]
|
153 |
-
table_frame, data_frame = self.theme.create_styled_data_table(self.root, columns, data)
|
154 |
-
self.assertIsNotNone(table_frame)
|
155 |
-
self.assertIsNotNone(data_frame)
|
156 |
-
self.assertEqual(table_frame.cget("fg_color"), self.theme.get_color("card_bg_color"))
|
157 |
-
|
158 |
-
def test_theme_switching(self):
|
159 |
-
"""اختبار تبديل النمط"""
|
160 |
-
# تعيين النمط الفاتح
|
161 |
-
self.theme.set_theme("light")
|
162 |
-
light_bg_color = self.theme.get_color("bg_color")
|
163 |
-
|
164 |
-
# تعيين النمط الداكن
|
165 |
-
self.theme.set_theme("dark")
|
166 |
-
dark_bg_color = self.theme.get_color("bg_color")
|
167 |
-
|
168 |
-
# التحقق من اختلاف الألوان
|
169 |
-
self.assertNotEqual(light_bg_color, dark_bg_color)
|
170 |
-
|
171 |
-
def test_language_switching(self):
|
172 |
-
"""اختبار تبديل اللغة"""
|
173 |
-
# تعيين اللغة العربية
|
174 |
-
self.theme.set_language("ar")
|
175 |
-
ar_font = self.theme.get_font("body")
|
176 |
-
|
177 |
-
# تعيين اللغة الإنجليزية
|
178 |
-
self.theme.set_language("en")
|
179 |
-
en_font = self.theme.get_font("body")
|
180 |
-
|
181 |
-
# التحقق من اختلاف الخطوط
|
182 |
-
self.assertNotEqual(ar_font[0], en_font[0])
|
183 |
-
|
184 |
-
|
185 |
-
class TestUILayout(unittest.TestCase):
|
186 |
-
"""اختبار تخطيط واجهة المستخدم"""
|
187 |
-
|
188 |
-
def setUp(self):
|
189 |
-
"""إعداد بيئة الاختبار"""
|
190 |
-
self.root = ctk.CTk()
|
191 |
-
self.root.withdraw() # إخفاء النافذة أثناء الاختبار
|
192 |
-
self.theme = AppTheme()
|
193 |
-
|
194 |
-
# إنشاء الإطار الرئيسي
|
195 |
-
self.main_frame = self.theme.create_styled_frame(self.root)
|
196 |
-
self.main_frame.pack(fill="both", expand=True)
|
197 |
-
|
198 |
-
# إنشاء الشريط الجانبي
|
199 |
-
self.sidebar_frame = self.theme.create_styled_frame(
|
200 |
-
self.main_frame,
|
201 |
-
fg_color=self.theme.get_color("sidebar_bg_color")
|
202 |
-
)
|
203 |
-
self.sidebar_frame.pack(side="left", fill="y", padx=0, pady=0)
|
204 |
-
|
205 |
-
# إنشاء إطار المحتوى
|
206 |
-
self.content_frame = self.theme.create_styled_frame(
|
207 |
-
self.main_frame,
|
208 |
-
fg_color=self.theme.get_color("bg_color")
|
209 |
-
)
|
210 |
-
self.content_frame.pack(side="right", fill="both", expand=True, padx=0, pady=0)
|
211 |
-
|
212 |
-
def tearDown(self):
|
213 |
-
"""تنظيف بيئة الاخ��بار"""
|
214 |
-
self.root.destroy()
|
215 |
-
|
216 |
-
def test_sidebar_layout(self):
|
217 |
-
"""اختبار تخطيط الشريط الجانبي"""
|
218 |
-
# إنشاء شعار التطبيق
|
219 |
-
logo_label = self.theme.create_styled_label(
|
220 |
-
self.sidebar_frame,
|
221 |
-
"نظام إدارة المناقصات",
|
222 |
-
font=self.theme.get_font("title"),
|
223 |
-
text_color=self.theme.get_color("sidebar_fg_color")
|
224 |
-
)
|
225 |
-
logo_label.pack(padx=20, pady=20)
|
226 |
-
|
227 |
-
# إنشاء أزرار الشريط الجانبي
|
228 |
-
sidebar_buttons = []
|
229 |
-
button_texts = [
|
230 |
-
"لوحة التحكم", "المشاريع", "المستندات", "التسعير",
|
231 |
-
"الموارد", "المخاطر", "التقارير", "الذكاء الاصطناعي"
|
232 |
-
]
|
233 |
-
|
234 |
-
for text in button_texts:
|
235 |
-
button_frame, button = self.theme.create_styled_sidebar_button(
|
236 |
-
self.sidebar_frame,
|
237 |
-
text
|
238 |
-
)
|
239 |
-
button_frame.pack(fill="x", padx=0, pady=2)
|
240 |
-
sidebar_buttons.append(button)
|
241 |
-
|
242 |
-
# التحقق من إنشاء الأزرار
|
243 |
-
self.assertEqual(len(sidebar_buttons), len(button_texts))
|
244 |
-
for i, button in enumerate(sidebar_buttons):
|
245 |
-
self.assertEqual(button.cget("text"), button_texts[i])
|
246 |
-
|
247 |
-
def test_content_layout(self):
|
248 |
-
"""اختبار تخطيط المحتوى"""
|
249 |
-
# إنشاء شريط العنوان
|
250 |
-
header_frame = self.theme.create_styled_frame(
|
251 |
-
self.content_frame,
|
252 |
-
fg_color=self.theme.get_color("card_bg_color")
|
253 |
-
)
|
254 |
-
header_frame.pack(fill="x", padx=20, pady=20)
|
255 |
-
|
256 |
-
# إنشاء عنوان الصفحة
|
257 |
-
page_title = self.theme.create_styled_label(
|
258 |
-
header_frame,
|
259 |
-
"لوحة التحكم",
|
260 |
-
font=self.theme.get_font("title")
|
261 |
-
)
|
262 |
-
page_title.pack(side="left", padx=20, pady=20)
|
263 |
-
|
264 |
-
# إنشاء زر البحث
|
265 |
-
search_button = self.theme.create_styled_button(
|
266 |
-
header_frame,
|
267 |
-
"بحث"
|
268 |
-
)
|
269 |
-
search_button.pack(side="right", padx=20, pady=20)
|
270 |
-
|
271 |
-
# إنشاء إطار البطاقات
|
272 |
-
cards_frame = self.theme.create_styled_frame(
|
273 |
-
self.content_frame,
|
274 |
-
fg_color="transparent"
|
275 |
-
)
|
276 |
-
cards_frame.pack(fill="both", expand=True, padx=20, pady=20)
|
277 |
-
|
278 |
-
# إنشاء بطاقات
|
279 |
-
cards = []
|
280 |
-
card_titles = [
|
281 |
-
"المشاريع النشطة", "المناقصات الجديدة", "المخاطر العالية", "التقارير المعلقة"
|
282 |
-
]
|
283 |
-
|
284 |
-
for i, title in enumerate(card_titles):
|
285 |
-
card, card_content = self.theme.create_styled_card(
|
286 |
-
cards_frame,
|
287 |
-
title
|
288 |
-
)
|
289 |
-
card.grid(row=i//2, column=i%2, padx=10, pady=10, sticky="nsew")
|
290 |
-
cards.append(card)
|
291 |
-
|
292 |
-
# التحقق من إنشاء البطاقات
|
293 |
-
self.assertEqual(len(cards), len(card_titles))
|
294 |
-
|
295 |
-
# تهيئة أوزان الصفوف والأعمدة
|
296 |
-
cards_frame.grid_columnconfigure(0, weight=1)
|
297 |
-
cards_frame.grid_columnconfigure(1, weight=1)
|
298 |
-
cards_frame.grid_rowconfigure(0, weight=1)
|
299 |
-
cards_frame.grid_rowconfigure(1, weight=1)
|
300 |
-
|
301 |
-
def test_responsive_layout(self):
|
302 |
-
"""اختبار التخطيط المتجاوب"""
|
303 |
-
# تغيير حجم النافذة
|
304 |
-
self.root.geometry("800x600")
|
305 |
-
self.root.update()
|
306 |
-
|
307 |
-
# التحقق من أن الإطار الرئيسي يملأ النافذة
|
308 |
-
self.assertEqual(self.main_frame.winfo_width(), 800)
|
309 |
-
self.assertEqual(self.main_frame.winfo_height(), 600)
|
310 |
-
|
311 |
-
# تغيير حجم النافذة مرة أخرى
|
312 |
-
self.root.geometry("1024x768")
|
313 |
-
self.root.update()
|
314 |
-
|
315 |
-
# التحقق من أن الإطار الرئيسي يملأ النافذة
|
316 |
-
self.assertEqual(self.main_frame.winfo_width(), 1024)
|
317 |
-
self.assertEqual(self.main_frame.winfo_height(), 768)
|
318 |
-
|
319 |
-
|
320 |
-
class TestUIArabicSupport(unittest.TestCase):
|
321 |
-
"""اختبار دعم اللغة العربية في واجهة المستخدم"""
|
322 |
-
|
323 |
-
def setUp(self):
|
324 |
-
"""إعداد بيئة الاختبار"""
|
325 |
-
self.root = ctk.CTk()
|
326 |
-
self.root.withdraw() # إخفاء النافذة أثناء الاختبار
|
327 |
-
self.theme = AppTheme()
|
328 |
-
self.theme.set_language("ar") # تعيين اللغة العربية
|
329 |
-
|
330 |
-
def tearDown(self):
|
331 |
-
"""تنظيف بيئة الاختبار"""
|
332 |
-
self.root.destroy()
|
333 |
-
|
334 |
-
def test_arabic_text_display(self):
|
335 |
-
"""اختبار عرض النص العربي"""
|
336 |
-
# إنشاء تسمية بنص عربي
|
337 |
-
arabic_text = "هذا نص عربي للاختبار"
|
338 |
-
label = self.theme.create_styled_label(self.root, arabic_text)
|
339 |
-
self.assertEqual(label.cget("text"), arabic_text)
|
340 |
-
|
341 |
-
# إنشاء زر بنص عربي
|
342 |
-
button = self.theme.create_styled_button(self.root, "زر باللغة العربية")
|
343 |
-
self.assertEqual(button.cget("text"), "زر باللغة العربية")
|
344 |
-
|
345 |
-
# إنشاء حقل إدخال بنص توضيحي عربي
|
346 |
-
entry = self.theme.create_styled_entry(self.root, "أدخل النص هنا")
|
347 |
-
self.assertEqual(entry.cget("placeholder_text"), "أدخل النص هنا")
|
348 |
-
|
349 |
-
def test_arabic_font(self):
|
350 |
-
"""اختبار الخط العربي"""
|
351 |
-
# التحقق من استخدام خط يدعم العربية
|
352 |
-
ar_font = self.theme.get_font("body")
|
353 |
-
self.assertEqual(ar_font[0], "Cairo")
|
354 |
-
|
355 |
-
def test_rtl_support(self):
|
356 |
-
"""اختبار دعم الكتابة من اليمين إلى اليسار"""
|
357 |
-
# إنشاء إطار
|
358 |
-
frame = self.theme.create_styled_frame(self.root)
|
359 |
-
frame.pack(fill="both", expand=True)
|
360 |
-
|
361 |
-
# إنشاء تسمية بنص عربي
|
362 |
-
label = self.theme.create_styled_label(frame, "نص عربي من اليمين إلى اليسار")
|
363 |
-
label.pack(anchor="e", padx=20, pady=20) # محاذاة إلى اليمين
|
364 |
-
|
365 |
-
# التحقق من المحاذاة
|
366 |
-
self.assertEqual(label.cget("anchor"), "w") # w تعني غرب (يسار)، لكن النص سيظهر من اليمين إلى اليسار
|
367 |
-
|
368 |
-
|
369 |
-
def run_ui_tests():
|
370 |
-
"""تشغيل اختبارات واجهة المستخدم"""
|
371 |
-
# إنشاء مجلد الاختبارات
|
372 |
-
test_dir = Path('test_results')
|
373 |
-
test_dir.mkdir(exist_ok=True)
|
374 |
-
|
375 |
-
# إنشاء ملف لنتائج الاختبارات
|
376 |
-
test_results_file = test_dir / 'ui_test_results.txt'
|
377 |
-
|
378 |
-
# تشغيل الاختبارات وحفظ النتائج
|
379 |
-
with open(test_results_file, 'w', encoding='utf-8') as f:
|
380 |
-
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
381 |
-
suite = unittest.TestSuite()
|
382 |
-
|
383 |
-
# إضافة اختبارات مكونات واجهة المستخدم
|
384 |
-
suite.addTest(unittest.makeSuite(TestUIComponents))
|
385 |
-
|
386 |
-
# إضافة اختبارات تخطيط واجهة المستخدم
|
387 |
-
suite.addTest(unittest.makeSuite(TestUILayout))
|
388 |
-
|
389 |
-
# إضافة اختبارات دعم اللغة العربية
|
390 |
-
suite.addTest(unittest.makeSuite(TestUIArabicSupport))
|
391 |
-
|
392 |
-
# تشغيل الاختبارات
|
393 |
-
result = runner.run(suite)
|
394 |
-
|
395 |
-
# كتابة ملخص النتائج
|
396 |
-
f.write("\n\n=== ملخص نتائج اختبارات واجهة المستخدم ===\n")
|
397 |
-
f.write(f"عدد الاختبارات: {result.testsRun}\n")
|
398 |
-
f.write(f"عدد النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}\n")
|
399 |
-
f.write(f"عدد الإخفاقات: {len(result.failures)}\n")
|
400 |
-
f.write(f"عدد الأخطاء: {len(result.errors)}\n")
|
401 |
-
|
402 |
-
# طباعة ملخص النتائج
|
403 |
-
logger.info(f"تم تشغيل {result.testsRun} اختبار لواجهة المستخدم")
|
404 |
-
logger.info(f"النجاحات: {result.testsRun - len(result.failures) - len(result.errors)}")
|
405 |
-
logger.info(f"الإخفاقات: {len(result.failures)}")
|
406 |
-
logger.info(f"الأخطاء: {len(result.errors)}")
|
407 |
-
logger.info(f"تم حفظ نتائج الاختبارات في: {test_results_file}")
|
408 |
-
|
409 |
-
return result
|
410 |
-
|
411 |
-
|
412 |
-
if __name__ == "__main__":
|
413 |
-
run_ui_tests()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/unit/test_reports.py
DELETED
@@ -1,200 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
اختبارات وحدة تقارير المشاريع
|
3 |
-
"""
|
4 |
-
|
5 |
-
import unittest
|
6 |
-
import pandas as pd
|
7 |
-
import numpy as np
|
8 |
-
import sys
|
9 |
-
import os
|
10 |
-
from datetime import datetime, timedelta
|
11 |
-
|
12 |
-
# إضافة المسار الرئيسي للمشروع لاستيراد الوحدات
|
13 |
-
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
14 |
-
|
15 |
-
from modules.reports.reports_app import ReportsApp
|
16 |
-
|
17 |
-
|
18 |
-
class TestReportsApp(unittest.TestCase):
|
19 |
-
"""اختبارات وحدة لتطبيق التقارير"""
|
20 |
-
|
21 |
-
def setUp(self):
|
22 |
-
"""إعداد بيئة الاختبار"""
|
23 |
-
self.reports_app = ReportsApp()
|
24 |
-
|
25 |
-
# إنشاء بيانات محاكاة للمشاريع
|
26 |
-
self.mock_projects = [
|
27 |
-
{
|
28 |
-
'id': 1,
|
29 |
-
'name': 'مشروع 1',
|
30 |
-
'number': 'T-2024001',
|
31 |
-
'client': 'وزارة الصحة',
|
32 |
-
'location': 'الرياض',
|
33 |
-
'status': 'جديد',
|
34 |
-
'submission_date': '2024-01-15',
|
35 |
-
'tender_type': 'عامة',
|
36 |
-
'created_at': '2024-01-10'
|
37 |
-
},
|
38 |
-
{
|
39 |
-
'id': 2,
|
40 |
-
'name': 'مشروع 2',
|
41 |
-
'number': 'T-2024002',
|
42 |
-
'client': 'وزارة التعليم',
|
43 |
-
'location': 'جدة',
|
44 |
-
'status': 'قيد التسعير',
|
45 |
-
'submission_date': '2024-01-28',
|
46 |
-
'tender_type': 'خاصة',
|
47 |
-
'created_at': '2024-01-20'
|
48 |
-
},
|
49 |
-
{
|
50 |
-
'id': 3,
|
51 |
-
'name': 'مشروع 3',
|
52 |
-
'number': 'T-2024003',
|
53 |
-
'client': 'وزارة الصحة',
|
54 |
-
'location': 'الدمام',
|
55 |
-
'status': 'تم التقديم',
|
56 |
-
'submission_date': '2024-02-10',
|
57 |
-
'tender_type': 'عامة',
|
58 |
-
'created_at': '2024-02-01'
|
59 |
-
},
|
60 |
-
{
|
61 |
-
'id': 4,
|
62 |
-
'name': 'مشروع 4',
|
63 |
-
'number': 'T-2024004',
|
64 |
-
'client': 'شركة أرامكو',
|
65 |
-
'location': 'الظهران',
|
66 |
-
'status': 'تمت الترسية',
|
67 |
-
'submission_date': '2024-02-15',
|
68 |
-
'tender_type': 'عامة',
|
69 |
-
'created_at': '2024-02-05'
|
70 |
-
},
|
71 |
-
{
|
72 |
-
'id': 5,
|
73 |
-
'name': 'مشروع 5',
|
74 |
-
'number': 'T-2024005',
|
75 |
-
'client': 'شركة سابك',
|
76 |
-
'location': 'الجبيل',
|
77 |
-
'status': 'قيد التنفيذ',
|
78 |
-
'submission_date': '2024-02-20',
|
79 |
-
'tender_type': 'خاصة',
|
80 |
-
'created_at': '2024-02-10'
|
81 |
-
}
|
82 |
-
]
|
83 |
-
|
84 |
-
def test_get_total_projects(self):
|
85 |
-
"""اختبار حساب إجمالي عدد المشاريع"""
|
86 |
-
# تعيين المشاريع في حالة الجلسة
|
87 |
-
import streamlit as st
|
88 |
-
st.session_state.projects = self.mock_projects
|
89 |
-
|
90 |
-
# اختبار الدالة
|
91 |
-
total = self.reports_app._get_total_projects()
|
92 |
-
self.assertEqual(total, 5)
|
93 |
-
|
94 |
-
def test_get_active_projects(self):
|
95 |
-
"""اختبار حساب عدد المشاريع النشطة"""
|
96 |
-
# تعيين المشاريع في حالة الجلسة
|
97 |
-
import streamlit as st
|
98 |
-
st.session_state.projects = self.mock_projects
|
99 |
-
|
100 |
-
# اختبار الدالة
|
101 |
-
active = self.reports_app._get_active_projects()
|
102 |
-
self.assertEqual(active, 4) # جميع المشاريع باستثناء الجديدة أو المنتهية أو الملغية
|
103 |
-
|
104 |
-
def test_get_won_projects(self):
|
105 |
-
"""اختبار حساب عدد المشاريع المرساة"""
|
106 |
-
# تعيين المشاريع في حالة الجلسة
|
107 |
-
import streamlit as st
|
108 |
-
st.session_state.projects = self.mock_projects
|
109 |
-
|
110 |
-
# اختبار الدالة
|
111 |
-
won = self.reports_app._get_won_projects()
|
112 |
-
self.assertEqual(won, 2) # المشاريع المرساة وقيد التنفيذ والمنتهية
|
113 |
-
|
114 |
-
def test_get_project_status_data(self):
|
115 |
-
"""اختبار الحصول على بيانات توزيع المشاريع حسب الحالة"""
|
116 |
-
# تعيين المشاريع في حالة الجلسة
|
117 |
-
import streamlit as st
|
118 |
-
st.session_state.projects = self.mock_projects
|
119 |
-
|
120 |
-
# اختبار الدالة
|
121 |
-
status_data = self.reports_app._get_project_status_data()
|
122 |
-
|
123 |
-
self.assertIsInstance(status_data, pd.DataFrame)
|
124 |
-
self.assertEqual(len(status_data), 5) # 5 حالات مختلفة
|
125 |
-
|
126 |
-
# التحقق من أن عدد المشاريع لكل حالة صحيح
|
127 |
-
status_counts = status_data.set_index('status')['count'].to_dict()
|
128 |
-
self.assertEqual(status_counts['جديد'], 1)
|
129 |
-
self.assertEqual(status_counts['قيد التسعير'], 1)
|
130 |
-
self.assertEqual(status_counts['تم التقديم'], 1)
|
131 |
-
self.assertEqual(status_counts['تمت الترسية'], 1)
|
132 |
-
self.assertEqual(status_counts['قيد التنفيذ'], 1)
|
133 |
-
|
134 |
-
def test_get_monthly_project_data(self):
|
135 |
-
"""اختبار الحصول على بيانات اتجاه المشاريع الشهري"""
|
136 |
-
# اختبار الدالة
|
137 |
-
monthly_data = self.reports_app._get_monthly_project_data()
|
138 |
-
|
139 |
-
self.assertIsInstance(monthly_data, pd.DataFrame)
|
140 |
-
self.assertEqual(len(monthly_data), 6) # 6 أشهر
|
141 |
-
|
142 |
-
# التحقق من وجود الأعمدة المطلوبة
|
143 |
-
self.assertIn('month', monthly_data.columns)
|
144 |
-
self.assertIn('new', monthly_data.columns)
|
145 |
-
self.assertIn('submitted', monthly_data.columns)
|
146 |
-
self.assertIn('won', monthly_data.columns)
|
147 |
-
|
148 |
-
def test_get_project_value_data(self):
|
149 |
-
"""اختبار الحصول على بيانات توزيع قيم المشاريع"""
|
150 |
-
# اختبار الدالة
|
151 |
-
value_data = self.reports_app._get_project_value_data()
|
152 |
-
|
153 |
-
self.assertIsInstance(value_data, pd.DataFrame)
|
154 |
-
self.assertEqual(len(value_data), 7) # 7 نطاقات قيمة
|
155 |
-
|
156 |
-
# التحقق من وجود الأعمدة المطلوبة
|
157 |
-
self.assertIn('range', value_data.columns)
|
158 |
-
self.assertIn('count', value_data.columns)
|
159 |
-
|
160 |
-
def test_generate_sample_data(self):
|
161 |
-
"""اختبار توليد البيانات العشوائية للمحاكاة"""
|
162 |
-
# مشاريع
|
163 |
-
project_fields = ["اسم المشروع", "رقم المناقصة", "العميل", "الموقع", "الحالة"]
|
164 |
-
project_data = self.reports_app._generate_sample_data("المشاريع", project_fields, 10)
|
165 |
-
|
166 |
-
self.assertIsInstance(project_data, pd.DataFrame)
|
167 |
-
self.assertEqual(len(project_data), 10)
|
168 |
-
for field in project_fields:
|
169 |
-
self.assertIn(field, project_data.columns)
|
170 |
-
|
171 |
-
# تسعير
|
172 |
-
pricing_fields = ["اسم المشروع", "رقم البند", "وصف البند", "الكمية", "سعر الوحدة"]
|
173 |
-
pricing_data = self.reports_app._generate_sample_data("التسعير", pricing_fields, 10)
|
174 |
-
|
175 |
-
self.assertIsInstance(pricing_data, pd.DataFrame)
|
176 |
-
self.assertEqual(len(pricing_data), 10)
|
177 |
-
for field in pricing_fields:
|
178 |
-
self.assertIn(field, pricing_data.columns)
|
179 |
-
|
180 |
-
# مخاطر
|
181 |
-
risk_fields = ["اسم المشروع", "رمز المخاطرة", "وصف المخاطرة", "الفئة"]
|
182 |
-
risk_data = self.reports_app._generate_sample_data("المخاطر", risk_fields, 10)
|
183 |
-
|
184 |
-
self.assertIsInstance(risk_data, pd.DataFrame)
|
185 |
-
self.assertEqual(len(risk_data), 10)
|
186 |
-
for field in risk_fields:
|
187 |
-
self.assertIn(field, risk_data.columns)
|
188 |
-
|
189 |
-
# محتوى محلي
|
190 |
-
local_content_fields = ["اسم المشروع", "الفئة", "البند", "المورد", "التكلفة"]
|
191 |
-
local_content_data = self.reports_app._generate_sample_data("المحتوى المحلي", local_content_fields, 10)
|
192 |
-
|
193 |
-
self.assertIsInstance(local_content_data, pd.DataFrame)
|
194 |
-
self.assertEqual(len(local_content_data), 10)
|
195 |
-
for field in local_content_fields:
|
196 |
-
self.assertIn(field, local_content_data.columns)
|
197 |
-
|
198 |
-
|
199 |
-
if __name__ == '__main__':
|
200 |
-
unittest.main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|