dash-devtools 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. dash_devtools/__init__.py +8 -0
  2. dash_devtools/__main__.py +11 -0
  3. dash_devtools/ai_engine.py +441 -0
  4. dash_devtools/browser.py +541 -0
  5. dash_devtools/cli.py +1452 -0
  6. dash_devtools/database.py +338 -0
  7. dash_devtools/dbdiagram.py +183 -0
  8. dash_devtools/e2e.py +329 -0
  9. dash_devtools/fixers/__init__.py +57 -0
  10. dash_devtools/fixers/migration_fixer.py +115 -0
  11. dash_devtools/fixers/ux_fixer.py +106 -0
  12. dash_devtools/fixers/version_bumper.py +115 -0
  13. dash_devtools/gas_mes_test.py +1241 -0
  14. dash_devtools/generators/__init__.py +84 -0
  15. dash_devtools/health.py +476 -0
  16. dash_devtools/hooks/__init__.py +250 -0
  17. dash_devtools/hooks/pre_commit.py +161 -0
  18. dash_devtools/hooks/pre_push.py +275 -0
  19. dash_devtools/init_test.py +352 -0
  20. dash_devtools/markdown_report.py +309 -0
  21. dash_devtools/migrators/__init__.py +21 -0
  22. dash_devtools/perf.py +321 -0
  23. dash_devtools/report.py +667 -0
  24. dash_devtools/reporters/__init__.py +11 -0
  25. dash_devtools/spec.py +230 -0
  26. dash_devtools/stats.py +355 -0
  27. dash_devtools/test_suite.py +690 -0
  28. dash_devtools/testing.py +416 -0
  29. dash_devtools/validators/__init__.py +157 -0
  30. dash_devtools/validators/backend/__init__.py +12 -0
  31. dash_devtools/validators/backend/nodejs.py +245 -0
  32. dash_devtools/validators/backend/python.py +439 -0
  33. dash_devtools/validators/code_quality.py +243 -0
  34. dash_devtools/validators/common/__init__.py +11 -0
  35. dash_devtools/validators/common/quality.py +319 -0
  36. dash_devtools/validators/common/security.py +270 -0
  37. dash_devtools/validators/common/spec.py +273 -0
  38. dash_devtools/validators/detector.py +394 -0
  39. dash_devtools/validators/frontend/__init__.py +14 -0
  40. dash_devtools/validators/frontend/angular.py +245 -0
  41. dash_devtools/validators/frontend/gas.py +310 -0
  42. dash_devtools/validators/frontend/vite.py +539 -0
  43. dash_devtools/validators/migration.py +292 -0
  44. dash_devtools/validators/performance.py +167 -0
  45. dash_devtools/validators/security.py +205 -0
  46. dash_devtools/vision/__init__.py +368 -0
  47. dash_devtools/watch.py +266 -0
  48. dash_devtools/word_report.py +690 -0
  49. dash_devtools-1.0.0.dist-info/METADATA +834 -0
  50. dash_devtools-1.0.0.dist-info/RECORD +53 -0
  51. dash_devtools-1.0.0.dist-info/WHEEL +5 -0
  52. dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
  53. dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,690 @@
1
+ """
2
+ 四大類型測試套件整合模組
3
+
4
+ 支援測試類型:
5
+ - UIT (Unit Integration Testing): 單元測試 + 覆蓋率
6
+ - Smoke: 煙霧測試 (關鍵路徑快速驗證)
7
+ - E2E (End-to-End): 端對端測試
8
+ - UAT (User Acceptance Testing): 使用者驗收測試
9
+
10
+ 支援測試框架:
11
+ - Vitest (推薦)
12
+ - Jest
13
+ - Playwright (E2E/Smoke/UAT)
14
+ - Pytest
15
+ """
16
+
17
+ import json
18
+ import subprocess
19
+ import re
20
+ from pathlib import Path
21
+ from dataclasses import dataclass, field
22
+ from typing import Dict, List, Optional, Tuple
23
+ from datetime import datetime
24
+ from rich.console import Console
25
+ from rich.panel import Panel
26
+ from rich.table import Table
27
+ from rich.text import Text
28
+ from rich.progress import Progress, SpinnerColumn, TextColumn
29
+
30
+ console = Console()
31
+
32
+
33
+ @dataclass
34
+ class TestCase:
35
+ """單一測試案例"""
36
+ name: str
37
+ status: str # passed, failed, skipped
38
+ duration: float = 0.0
39
+ error: str = ""
40
+ screenshot: str = "" # 截圖路徑
41
+ api_response: str = "" # API 測試回應 (JSON)
42
+ terminal_output: str = "" # 終端輸出 (UIT)
43
+
44
+
45
+ @dataclass
46
+ class TestTypeResult:
47
+ """單一測試類型結果"""
48
+ test_type: str # UIT, Smoke, E2E, UAT
49
+ passed: int = 0
50
+ failed: int = 0
51
+ skipped: int = 0
52
+ duration: float = 0.0
53
+ coverage: float = 0.0
54
+ success: bool = True
55
+ error: str = ""
56
+ details: List[str] = field(default_factory=list)
57
+ test_cases: List[TestCase] = field(default_factory=list) # 測試案例列表
58
+ not_configured: bool = False # 該測試類型未設定 (優雅跳過)
59
+
60
+
61
+ @dataclass
62
+ class TestSuiteResult:
63
+ """完整測試套件結果"""
64
+ project_name: str
65
+ results: Dict[str, TestTypeResult] = field(default_factory=dict)
66
+ total_passed: int = 0
67
+ total_failed: int = 0
68
+ total_duration: float = 0.0
69
+ overall_success: bool = True
70
+ coverage: float = 0.0
71
+ timestamp: str = ""
72
+
73
+
74
+ class TestSuiteRunner:
75
+ """四大類型測試套件執行器"""
76
+
77
+ def __init__(self, project_path: str):
78
+ self.project_path = Path(project_path)
79
+ self.project_name = self.project_path.name
80
+
81
+ def detect_test_setup(self) -> Dict:
82
+ """偵測專案測試設定"""
83
+ setup = {
84
+ 'has_vitest': False,
85
+ 'has_jest': False,
86
+ 'has_playwright': False,
87
+ 'has_pytest': False,
88
+ 'package_scripts': {},
89
+ 'test_dirs': []
90
+ }
91
+
92
+ # 檢查 package.json
93
+ package_json = self.project_path / 'package.json'
94
+ if package_json.exists():
95
+ try:
96
+ pkg = json.loads(package_json.read_text())
97
+ deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
98
+ setup['package_scripts'] = pkg.get('scripts', {})
99
+
100
+ setup['has_vitest'] = 'vitest' in deps
101
+ setup['has_jest'] = 'jest' in deps
102
+ setup['has_playwright'] = '@playwright/test' in deps
103
+ except Exception:
104
+ pass
105
+
106
+ # 檢查測試目錄
107
+ for test_dir in ['e2e', 'tests', 'test', '__tests__', 'spec']:
108
+ if (self.project_path / test_dir).exists():
109
+ setup['test_dirs'].append(test_dir)
110
+
111
+ # 檢查 Playwright 設定
112
+ if (self.project_path / 'playwright.config.ts').exists():
113
+ setup['has_playwright'] = True
114
+
115
+ # 檢查 Python
116
+ if (self.project_path / 'pytest.ini').exists() or \
117
+ (self.project_path / 'pyproject.toml').exists():
118
+ setup['has_pytest'] = True
119
+
120
+ return setup
121
+
122
+ def run_uit(self, with_coverage: bool = True) -> TestTypeResult:
123
+ """執行 UIT 單元測試"""
124
+ result = TestTypeResult(test_type='UIT')
125
+ setup = self.detect_test_setup()
126
+
127
+ try:
128
+ if setup['has_vitest']:
129
+ # 使用 JSON reporter 取得詳細結果
130
+ cmd = ['npx', 'vitest', 'run', '--reporter=json']
131
+ if with_coverage:
132
+ cmd.append('--coverage')
133
+
134
+ proc = subprocess.run(
135
+ cmd,
136
+ cwd=self.project_path,
137
+ capture_output=True,
138
+ text=True,
139
+ timeout=300
140
+ )
141
+
142
+ result.success = proc.returncode == 0
143
+ output = proc.stdout + proc.stderr
144
+
145
+ # 移除 ANSI 顏色碼
146
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
147
+ clean_output = ansi_escape.sub('', output)
148
+
149
+ # 嘗試解析 JSON 並建立摘要
150
+ terminal_summary = ""
151
+ try:
152
+ # 找到 JSON 部分 (Vitest JSON 輸出)
153
+ json_match = re.search(r'(\{[\s\S]*"testResults"[\s\S]*\})', proc.stdout)
154
+ if json_match:
155
+ json_data = json.loads(json_match.group(1))
156
+
157
+ # 從 JSON 提取統計數據
158
+ result.passed = json_data.get('numPassedTests', 0)
159
+ result.failed = json_data.get('numFailedTests', 0)
160
+ num_total = json_data.get('numTotalTests', 0)
161
+ num_suites = json_data.get('numTotalTestSuites', 0)
162
+
163
+ # 解析覆蓋率 (從 stderr)
164
+ coverage_match = re.search(r'All files\s+\|\s+([\d.]+)', clean_output)
165
+ if coverage_match:
166
+ result.coverage = float(coverage_match.group(1))
167
+
168
+ # 從 JSON 建立人類可讀的摘要
169
+ summary_parts = [
170
+ f"Test Suites: {num_suites}",
171
+ f"Tests: {result.passed} passed" + (f", {result.failed} failed" if result.failed else ""),
172
+ f"Total: {num_total} tests"
173
+ ]
174
+ if result.coverage > 0:
175
+ summary_parts.append(f"Coverage: {result.coverage:.1f}%")
176
+
177
+ terminal_summary = '\n'.join(summary_parts)
178
+
179
+ for test_file in json_data.get('testResults', []):
180
+ file_name = Path(test_file.get('name', '')).name
181
+ for assertion in test_file.get('assertionResults', []):
182
+ test_name = ' › '.join(assertion.get('ancestorTitles', []) + [assertion.get('title', '')])
183
+ status = assertion.get('status', 'passed')
184
+ # Vitest duration 是毫秒,轉為秒 (與 Playwright 統一)
185
+ duration = assertion.get('duration', 0) / 1000 # ms -> s
186
+ result.test_cases.append(TestCase(
187
+ name=f"{file_name} › {test_name}",
188
+ status=status,
189
+ duration=duration
190
+ # UIT 不顯示 terminal_output (統計已在報告摘要中)
191
+ ))
192
+ except (json.JSONDecodeError, KeyError):
193
+ # 備援:從輸出解析測試名稱
194
+ # 格式: ✓ src/app/core/services/warehouse.service.spec.ts (25 tests) 2ms
195
+ for match in re.finditer(r'[✓✗]\s+(\S+\.spec\.ts)\s+\((\d+)\s+tests?\)', clean_output):
196
+ file_name = Path(match.group(1)).name
197
+ test_count = int(match.group(2))
198
+ # 無法取得個別測試名稱,用檔案名代替
199
+ result.test_cases.append(TestCase(
200
+ name=f"{file_name} ({test_count} tests)",
201
+ status='passed' if '✓' in match.group(0) else 'failed'
202
+ ))
203
+
204
+ # 解析統計
205
+ match = re.search(r'Tests\s+(\d+)\s+passed', clean_output)
206
+ if match:
207
+ result.passed = int(match.group(1))
208
+ else:
209
+ match = re.search(r'(\d+)\s+passed', clean_output)
210
+ if match:
211
+ result.passed = int(match.group(1))
212
+
213
+ match = re.search(r'(\d+)\s+failed', clean_output)
214
+ if match:
215
+ result.failed = int(match.group(1))
216
+
217
+ # 解析覆蓋率
218
+ match = re.search(r'All files\s+\|\s+([\d.]+)', clean_output)
219
+ if match:
220
+ result.coverage = float(match.group(1))
221
+
222
+ # 解析時間
223
+ match = re.search(r'Duration\s+([\d.]+)ms', clean_output)
224
+ if match:
225
+ result.duration = float(match.group(1)) / 1000
226
+ else:
227
+ match = re.search(r'Duration\s+([\d.]+)s', clean_output)
228
+ if match:
229
+ result.duration = float(match.group(1))
230
+
231
+ elif setup['has_jest']:
232
+ cmd = ['npx', 'jest', '--coverage'] if with_coverage else ['npx', 'jest']
233
+ proc = subprocess.run(
234
+ cmd,
235
+ cwd=self.project_path,
236
+ capture_output=True,
237
+ text=True,
238
+ timeout=300
239
+ )
240
+
241
+ result.success = proc.returncode == 0
242
+ output = proc.stdout + proc.stderr
243
+
244
+ match = re.search(r'Tests:\s+(\d+) passed', output)
245
+ if match:
246
+ result.passed = int(match.group(1))
247
+
248
+ match = re.search(r'(\d+) failed', output)
249
+ if match:
250
+ result.failed = int(match.group(1))
251
+
252
+ elif setup['has_pytest']:
253
+ cmd = ['python', '-m', 'pytest', '--cov', '--cov-report=term'] if with_coverage \
254
+ else ['python', '-m', 'pytest', '-v']
255
+ proc = subprocess.run(
256
+ cmd,
257
+ cwd=self.project_path,
258
+ capture_output=True,
259
+ text=True,
260
+ timeout=300
261
+ )
262
+
263
+ result.success = proc.returncode == 0
264
+ output = proc.stdout + proc.stderr
265
+
266
+ match = re.search(r'(\d+) passed', output)
267
+ if match:
268
+ result.passed = int(match.group(1))
269
+
270
+ match = re.search(r'(\d+) failed', output)
271
+ if match:
272
+ result.failed = int(match.group(1))
273
+
274
+ match = re.search(r'TOTAL\s+\d+\s+\d+\s+(\d+)%', output)
275
+ if match:
276
+ result.coverage = float(match.group(1))
277
+
278
+ else:
279
+ # 無測試框架,優雅跳過 (不視為失敗)
280
+ result.success = True
281
+ result.not_configured = True
282
+ result.error = "未設定單元測試框架"
283
+
284
+ except subprocess.TimeoutExpired:
285
+ result.success = False
286
+ result.error = "測試超時 (5分鐘)"
287
+ except Exception as e:
288
+ result.success = False
289
+ result.error = str(e)
290
+
291
+ return result
292
+
293
+ def run_playwright_tests(self, spec_pattern: str, test_type: str, capture_screenshots: bool = True) -> TestTypeResult:
294
+ """執行 Playwright 測試"""
295
+ result = TestTypeResult(test_type=test_type)
296
+ setup = self.detect_test_setup()
297
+
298
+ try:
299
+ # 檢查是否有 Playwright
300
+ if not setup['has_playwright']:
301
+ result.success = True
302
+ result.not_configured = True
303
+ result.error = "未安裝 Playwright"
304
+ return result
305
+
306
+ # 檢查是否有對應的測試檔案
307
+ spec_files = list(self.project_path.glob(f'e2e/{spec_pattern}'))
308
+ if not spec_files:
309
+ result.success = True
310
+ result.not_configured = True
311
+ result.error = f"未找到 {spec_pattern}"
312
+ return result
313
+
314
+ # 為每個測試類型建立獨立的輸出目錄
315
+ output_dir = self.project_path / 'test-results' / test_type.lower()
316
+ output_dir.mkdir(parents=True, exist_ok=True)
317
+
318
+ # 使用 JSON reporter 取得詳細結果
319
+ cmd = [
320
+ 'npx', 'playwright', 'test', f'e2e/{spec_pattern}',
321
+ '--reporter=json',
322
+ f'--output={output_dir}'
323
+ ]
324
+
325
+ # 設定環境變數啟用截圖
326
+ env = dict(subprocess.os.environ)
327
+ if capture_screenshots:
328
+ env['SCREENSHOT_ALL'] = '1'
329
+
330
+ proc = subprocess.run(
331
+ cmd,
332
+ cwd=self.project_path,
333
+ capture_output=True,
334
+ text=True,
335
+ timeout=300,
336
+ env=env
337
+ )
338
+
339
+ output = proc.stdout + proc.stderr
340
+ result.success = proc.returncode == 0
341
+
342
+ # 嘗試解析 JSON 輸出
343
+ try:
344
+ # Playwright JSON 輸出在 stdout
345
+ json_data = json.loads(proc.stdout)
346
+
347
+ # 解析測試案例
348
+ for suite in json_data.get('suites', []):
349
+ self._parse_playwright_suite(suite, result)
350
+
351
+ # 計算統計
352
+ result.passed = sum(1 for tc in result.test_cases if tc.status == 'passed')
353
+ result.failed = sum(1 for tc in result.test_cases if tc.status == 'failed')
354
+ result.skipped = sum(1 for tc in result.test_cases if tc.status == 'skipped')
355
+
356
+ except json.JSONDecodeError:
357
+ # 備援:用正則解析
358
+ # 移除 ANSI 碼
359
+ ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
360
+ clean_output = ansi_escape.sub('', output)
361
+
362
+ match = re.search(r'(\d+) passed', clean_output)
363
+ if match:
364
+ result.passed = int(match.group(1))
365
+
366
+ match = re.search(r'(\d+) failed', clean_output)
367
+ if match:
368
+ result.failed = int(match.group(1))
369
+
370
+ # 解析測試名稱 (從輸出中提取)
371
+ # 格式: [chromium] › e2e/smoke.spec.ts:11:7 › Smoke Tests › SMOKE-01: 應用程式啟動
372
+ for match in re.finditer(r'› ([^›]+\.spec\.ts:\d+:\d+) › (.+)', clean_output):
373
+ test_name = match.group(2).strip()
374
+ # 判斷狀態
375
+ status = 'passed'
376
+ if '✓' in clean_output or 'passed' in clean_output:
377
+ status = 'passed'
378
+ result.test_cases.append(TestCase(name=test_name, status=status))
379
+
380
+ match = re.search(r'\(([\d.]+)s\)', output)
381
+ if match:
382
+ result.duration = float(match.group(1))
383
+
384
+ except subprocess.TimeoutExpired:
385
+ result.success = False
386
+ result.error = "測試超時"
387
+ except Exception as e:
388
+ result.success = False
389
+ result.error = str(e)
390
+
391
+ return result
392
+
393
+ def _parse_playwright_suite(self, suite: Dict, result: TestTypeResult, prefix: str = ""):
394
+ """遞迴解析 Playwright 測試套件"""
395
+ suite_title = suite.get('title', '')
396
+ current_prefix = f"{prefix} › {suite_title}" if prefix else suite_title
397
+
398
+ # 解析 specs (測試案例)
399
+ for spec in suite.get('specs', []):
400
+ test_title = spec.get('title', '')
401
+ full_name = f"{current_prefix} › {test_title}" if current_prefix else test_title
402
+
403
+ # 取得測試結果
404
+ tests = spec.get('tests', [])
405
+ for test in tests:
406
+ results_list = test.get('results', [])
407
+ status = 'passed'
408
+ duration = 0.0
409
+ error = ''
410
+ screenshot = ''
411
+ api_response = ''
412
+
413
+ for res in results_list:
414
+ status = res.get('status', 'passed')
415
+ duration = res.get('duration', 0) / 1000 # 毫秒轉秒
416
+ if res.get('error'):
417
+ error = res['error'].get('message', '')[:200]
418
+
419
+ # 取得附件 (截圖或 API 回應)
420
+ attachments = res.get('attachments', [])
421
+ for att in attachments:
422
+ att_name = att.get('name', '')
423
+ if att_name == 'screenshot' and att.get('path'):
424
+ screenshot = att.get('path', '')
425
+ elif att_name == 'api-response' and att.get('body'):
426
+ # API 回應是 base64 編碼的 body
427
+ import base64
428
+ try:
429
+ body = att.get('body', '')
430
+ if body:
431
+ api_response = base64.b64decode(body).decode('utf-8')
432
+ except Exception:
433
+ api_response = att.get('body', '')
434
+
435
+ result.test_cases.append(TestCase(
436
+ name=full_name,
437
+ status=status,
438
+ duration=duration,
439
+ error=error,
440
+ screenshot=screenshot,
441
+ api_response=api_response
442
+ ))
443
+
444
+ # 遞迴處理子套件
445
+ for sub_suite in suite.get('suites', []):
446
+ self._parse_playwright_suite(sub_suite, result, current_prefix)
447
+
448
+ def run_smoke(self) -> TestTypeResult:
449
+ """執行 Smoke 煙霧測試"""
450
+ return self.run_playwright_tests('smoke.spec.ts', 'Smoke')
451
+
452
+ def run_e2e(self) -> TestTypeResult:
453
+ """執行 E2E 端對端測試"""
454
+ # 嘗試多種命名模式
455
+ patterns = ['mes-system.spec.ts', '*.e2e.spec.ts', 'e2e.spec.ts', '!smoke.spec.ts&!uat.spec.ts']
456
+ result = self.run_playwright_tests('mes-system.spec.ts', 'E2E')
457
+ if result.passed == 0 and not result.error:
458
+ result = self.run_playwright_tests('*.e2e.spec.ts', 'E2E')
459
+ return result
460
+
461
+ def run_uat(self) -> TestTypeResult:
462
+ """執行 UAT 使用者驗收測試"""
463
+ return self.run_playwright_tests('uat.spec.ts', 'UAT')
464
+
465
+ def run_all(self, test_types: List[str] = None) -> TestSuiteResult:
466
+ """執行所有測試類型"""
467
+ if test_types is None:
468
+ test_types = ['UIT', 'Smoke', 'E2E', 'UAT']
469
+
470
+ suite_result = TestSuiteResult(
471
+ project_name=self.project_name,
472
+ timestamp=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
473
+ )
474
+
475
+ for test_type in test_types:
476
+ if test_type.upper() == 'UIT':
477
+ result = self.run_uit(with_coverage=True)
478
+ elif test_type.upper() == 'SMOKE':
479
+ result = self.run_smoke()
480
+ elif test_type.upper() == 'E2E':
481
+ result = self.run_e2e()
482
+ elif test_type.upper() == 'UAT':
483
+ result = self.run_uat()
484
+ else:
485
+ continue
486
+
487
+ suite_result.results[test_type.upper()] = result
488
+ suite_result.total_passed += result.passed
489
+ suite_result.total_failed += result.failed
490
+ suite_result.total_duration += result.duration
491
+
492
+ if not result.success:
493
+ suite_result.overall_success = False
494
+
495
+ # 取得 UIT 覆蓋率
496
+ if 'UIT' in suite_result.results:
497
+ suite_result.coverage = suite_result.results['UIT'].coverage
498
+
499
+ return suite_result
500
+
501
+
502
+ def render_test_suite_result(suite: TestSuiteResult):
503
+ """渲染測試套件結果"""
504
+
505
+ # 標題
506
+ status_color = "green" if suite.overall_success else "red"
507
+ status_icon = "v" if suite.overall_success else "x"
508
+
509
+ title = Text()
510
+ title.append(f"\n {suite.project_name} ", style="bold white")
511
+ title.append("測試套件報告\n", style="dim")
512
+ console.print(Panel(title, border_style="cyan"))
513
+
514
+ # 測試類型結果表格
515
+ table = Table(show_header=True, header_style="bold cyan")
516
+ table.add_column("測試類型", style="white")
517
+ table.add_column("狀態", justify="center")
518
+ table.add_column("通過", justify="right", style="green")
519
+ table.add_column("失敗", justify="right", style="red")
520
+ table.add_column("時間", justify="right", style="dim")
521
+ table.add_column("覆蓋率", justify="right")
522
+
523
+ type_labels = {
524
+ 'UIT': '單元測試 (UIT)',
525
+ 'Smoke': '煙霧測試 (Smoke)',
526
+ 'E2E': '端對端測試 (E2E)',
527
+ 'UAT': '驗收測試 (UAT)'
528
+ }
529
+
530
+ configured_count = 0
531
+ for test_type, result in suite.results.items():
532
+ # 判斷狀態
533
+ if result.not_configured:
534
+ status = "[dim]N/A[/dim]"
535
+ elif result.success:
536
+ status = "[green]PASS[/green]"
537
+ configured_count += 1
538
+ else:
539
+ status = "[red]FAIL[/red]"
540
+ configured_count += 1
541
+
542
+ coverage_str = f"{result.coverage:.0f}%" if result.coverage > 0 else "-"
543
+
544
+ table.add_row(
545
+ type_labels.get(test_type, test_type),
546
+ status,
547
+ str(result.passed) if not result.not_configured else "-",
548
+ str(result.failed) if result.failed else "-",
549
+ f"{result.duration:.1f}s" if result.duration else "-",
550
+ coverage_str
551
+ )
552
+
553
+ console.print(table)
554
+
555
+ # 總計
556
+ console.print()
557
+ total = suite.total_passed + suite.total_failed
558
+ if total > 0:
559
+ pass_rate = (suite.total_passed / total) * 100
560
+
561
+ # 進度條
562
+ bar_width = 40
563
+ passed_width = int((suite.total_passed / total) * bar_width)
564
+ failed_width = bar_width - passed_width
565
+
566
+ bar = Text()
567
+ bar.append(" ")
568
+ bar.append("=" * passed_width, style="green")
569
+ bar.append("=" * failed_width, style="red")
570
+ bar.append(f" {pass_rate:.1f}%", style="green" if pass_rate >= 80 else "yellow")
571
+ console.print(bar)
572
+ console.print()
573
+
574
+ # 總結
575
+ summary = Text()
576
+ summary.append(" 總計: ")
577
+ summary.append(f"{suite.total_passed} passed", style="green")
578
+ summary.append(" / ")
579
+ summary.append(f"{suite.total_failed} failed", style="red" if suite.total_failed else "dim")
580
+ summary.append(f" | {suite.total_duration:.1f}s", style="dim")
581
+ console.print(summary)
582
+
583
+ # 覆蓋率
584
+ if suite.coverage > 0:
585
+ cov_color = "green" if suite.coverage >= 80 else "yellow" if suite.coverage >= 60 else "red"
586
+ console.print(f" 覆蓋率: [{cov_color}]{suite.coverage:.1f}%[/{cov_color}]")
587
+
588
+ # 最終狀態
589
+ console.print()
590
+ if configured_count == 0:
591
+ console.print(f" [yellow][-] 此專案未設定任何測試[/yellow]")
592
+ elif suite.overall_success:
593
+ console.print(f" [green][v] 所有測試通過[/green]")
594
+ else:
595
+ console.print(f" [red][x] 部分測試失敗[/red]")
596
+
597
+ console.print()
598
+
599
+ return {
600
+ 'success': suite.overall_success,
601
+ 'total_passed': suite.total_passed,
602
+ 'total_failed': suite.total_failed,
603
+ 'coverage': suite.coverage,
604
+ 'results': {k: {
605
+ 'passed': v.passed,
606
+ 'failed': v.failed,
607
+ 'success': v.success
608
+ } for k, v in suite.results.items()}
609
+ }
610
+
611
+
612
+ def run_test_suite(
613
+ project_path: str,
614
+ test_types: List[str] = None,
615
+ coverage: bool = True
616
+ ) -> dict:
617
+ """
618
+ 執行測試套件
619
+
620
+ Args:
621
+ project_path: 專案路徑
622
+ test_types: 要執行的測試類型列表 (預設全部)
623
+ coverage: 是否包含覆蓋率 (預設 True)
624
+
625
+ Returns:
626
+ 測試結果字典
627
+ """
628
+ runner = TestSuiteRunner(project_path)
629
+
630
+ with Progress(
631
+ SpinnerColumn(),
632
+ TextColumn("[progress.description]{task.description}"),
633
+ console=console
634
+ ) as progress:
635
+ # 偵測設定
636
+ task = progress.add_task("偵測測試設定...", total=None)
637
+ setup = runner.detect_test_setup()
638
+ progress.update(task, description="[green]設定偵測完成 v")
639
+
640
+ # 執行測試
641
+ task = progress.add_task("執行測試套件...", total=None)
642
+ suite_result = runner.run_all(test_types)
643
+ progress.update(task, description="[green]測試執行完成 v")
644
+
645
+ return render_test_suite_result(suite_result)
646
+
647
+
648
+ def run_test_suite_report(project_path: str, output_path: str = None) -> dict:
649
+ """
650
+ 產生測試套件報告
651
+
652
+ Args:
653
+ project_path: 專案路徑
654
+ output_path: 報告輸出路徑 (可選)
655
+
656
+ Returns:
657
+ 報告字典
658
+ """
659
+ runner = TestSuiteRunner(project_path)
660
+ suite_result = runner.run_all()
661
+
662
+ report = {
663
+ 'project': suite_result.project_name,
664
+ 'timestamp': suite_result.timestamp,
665
+ 'overall_success': suite_result.overall_success,
666
+ 'summary': {
667
+ 'total_passed': suite_result.total_passed,
668
+ 'total_failed': suite_result.total_failed,
669
+ 'total_duration': suite_result.total_duration,
670
+ 'coverage': suite_result.coverage
671
+ },
672
+ 'tests': {}
673
+ }
674
+
675
+ for test_type, result in suite_result.results.items():
676
+ report['tests'][test_type] = {
677
+ 'success': result.success,
678
+ 'passed': result.passed,
679
+ 'failed': result.failed,
680
+ 'duration': result.duration,
681
+ 'coverage': result.coverage,
682
+ 'error': result.error
683
+ }
684
+
685
+ if output_path:
686
+ output_file = Path(output_path)
687
+ output_file.write_text(json.dumps(report, indent=2, ensure_ascii=False))
688
+ console.print(f"[green]報告已儲存: {output_path}[/green]")
689
+
690
+ return report