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,416 @@
1
+ """
2
+ 測試執行與分析
3
+
4
+ 專注於測試的核心功能:
5
+ - 自動偵測測試框架
6
+ - 執行測試並收集結果
7
+ - 覆蓋率分析
8
+ - 測試健康指標
9
+ """
10
+
11
+ import json
12
+ import subprocess
13
+ import re
14
+ from pathlib import Path
15
+ from dataclasses import dataclass, field
16
+ from typing import Dict, List, Optional, Tuple
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+ from rich.text import Text
21
+ from rich.progress import Progress, SpinnerColumn, TextColumn
22
+
23
+ console = Console()
24
+
25
+
26
+ @dataclass
27
+ class TestCase:
28
+ """單一測試案例"""
29
+ name: str
30
+ status: str # passed, failed, skipped
31
+ duration: float = 0.0
32
+ error: str = ""
33
+
34
+
35
+ @dataclass
36
+ class TestSuite:
37
+ """測試套件"""
38
+ name: str
39
+ tests: List[TestCase] = field(default_factory=list)
40
+
41
+ @property
42
+ def passed(self) -> int:
43
+ return sum(1 for t in self.tests if t.status == 'passed')
44
+
45
+ @property
46
+ def failed(self) -> int:
47
+ return sum(1 for t in self.tests if t.status == 'failed')
48
+
49
+ @property
50
+ def skipped(self) -> int:
51
+ return sum(1 for t in self.tests if t.status == 'skipped')
52
+
53
+
54
+ @dataclass
55
+ class TestResult:
56
+ """測試執行結果"""
57
+ framework: str
58
+ success: bool = True
59
+ passed: int = 0
60
+ failed: int = 0
61
+ skipped: int = 0
62
+ duration: float = 0.0
63
+ coverage: float = 0.0
64
+ suites: List[TestSuite] = field(default_factory=list)
65
+ failed_tests: List[str] = field(default_factory=list)
66
+ output: str = ""
67
+
68
+
69
+ class TestRunner:
70
+ """測試執行器"""
71
+
72
+ FRAMEWORKS = {
73
+ 'vitest': {
74
+ 'detect': ['vitest'],
75
+ 'cmd': ['npx', 'vitest', 'run', '--reporter=json'],
76
+ 'coverage_cmd': ['npx', 'vitest', 'run', '--coverage']
77
+ },
78
+ 'jest': {
79
+ 'detect': ['jest'],
80
+ 'cmd': ['npx', 'jest', '--json', '--passWithNoTests'],
81
+ 'coverage_cmd': ['npx', 'jest', '--coverage', '--passWithNoTests']
82
+ },
83
+ 'pytest': {
84
+ 'detect': ['pytest'],
85
+ 'cmd': ['python', '-m', 'pytest', '-v', '--tb=short'],
86
+ 'coverage_cmd': ['python', '-m', 'pytest', '--cov', '--cov-report=term-missing']
87
+ },
88
+ 'karma': {
89
+ 'detect': ['@angular-devkit/build-angular', 'karma'],
90
+ 'cmd': ['npx', 'ng', 'test', '--no-watch', '--browsers=ChromeHeadless'],
91
+ 'coverage_cmd': ['npx', 'ng', 'test', '--no-watch', '--code-coverage', '--browsers=ChromeHeadless']
92
+ }
93
+ }
94
+
95
+ def __init__(self, project_path: str):
96
+ self.project_path = Path(project_path)
97
+ self.project_name = self.project_path.name
98
+
99
+ def detect_framework(self) -> Tuple[str, Dict]:
100
+ """偵測測試框架"""
101
+ # 檢查 package.json
102
+ package_json = self.project_path / 'package.json'
103
+ if package_json.exists():
104
+ try:
105
+ pkg = json.loads(package_json.read_text())
106
+ deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
107
+
108
+ for framework, config in self.FRAMEWORKS.items():
109
+ if framework == 'pytest':
110
+ continue
111
+ for detect_pkg in config['detect']:
112
+ if detect_pkg in deps:
113
+ return framework, config
114
+ except Exception:
115
+ pass
116
+
117
+ # 檢查 Python 專案
118
+ if (self.project_path / 'pytest.ini').exists() or \
119
+ (self.project_path / 'pyproject.toml').exists() or \
120
+ (self.project_path / 'setup.py').exists():
121
+ # 確認有 tests 目錄或 test_*.py 檔案
122
+ if (self.project_path / 'tests').exists() or \
123
+ list(self.project_path.rglob('test_*.py')):
124
+ return 'pytest', self.FRAMEWORKS['pytest']
125
+
126
+ return None, None
127
+
128
+ def run(self, with_coverage: bool = False, verbose: bool = False) -> TestResult:
129
+ """執行測試"""
130
+ framework, config = self.detect_framework()
131
+
132
+ if not framework:
133
+ return TestResult(
134
+ framework='unknown',
135
+ success=False,
136
+ output='未偵測到測試框架'
137
+ )
138
+
139
+ result = TestResult(framework=framework)
140
+ cmd = config['coverage_cmd'] if with_coverage else config['cmd']
141
+
142
+ try:
143
+ proc = subprocess.run(
144
+ cmd,
145
+ cwd=self.project_path,
146
+ capture_output=True,
147
+ text=True,
148
+ timeout=300 # 5 分鐘超時
149
+ )
150
+
151
+ result.output = proc.stdout + proc.stderr
152
+ result.success = proc.returncode == 0
153
+
154
+ # 解析結果
155
+ self._parse_result(result, framework, proc.stdout + proc.stderr)
156
+
157
+ except subprocess.TimeoutExpired:
158
+ result.success = False
159
+ result.output = "測試超時 (5分鐘)"
160
+ except FileNotFoundError as e:
161
+ result.success = False
162
+ result.output = f"找不到測試命令: {e}"
163
+ except Exception as e:
164
+ result.success = False
165
+ result.output = str(e)
166
+
167
+ return result
168
+
169
+ def _parse_result(self, result: TestResult, framework: str, output: str):
170
+ """解析測試結果"""
171
+ if framework == 'pytest':
172
+ self._parse_pytest(result, output)
173
+ elif framework in ['jest', 'vitest']:
174
+ self._parse_jest(result, output)
175
+ elif framework == 'karma':
176
+ self._parse_karma(result, output)
177
+
178
+ def _parse_pytest(self, result: TestResult, output: str):
179
+ """解析 pytest 輸出"""
180
+ # 解析通過/失敗數量
181
+ match = re.search(r'(\d+) passed', output)
182
+ if match:
183
+ result.passed = int(match.group(1))
184
+
185
+ match = re.search(r'(\d+) failed', output)
186
+ if match:
187
+ result.failed = int(match.group(1))
188
+
189
+ match = re.search(r'(\d+) skipped', output)
190
+ if match:
191
+ result.skipped = int(match.group(1))
192
+
193
+ # 解析執行時間
194
+ match = re.search(r'in ([\d.]+)s', output)
195
+ if match:
196
+ result.duration = float(match.group(1))
197
+
198
+ # 解析覆蓋率
199
+ match = re.search(r'TOTAL\s+\d+\s+\d+\s+(\d+)%', output)
200
+ if match:
201
+ result.coverage = float(match.group(1))
202
+
203
+ # 收集失敗的測試
204
+ for match in re.finditer(r'FAILED ([\w/.:]+)', output):
205
+ result.failed_tests.append(match.group(1))
206
+
207
+ def _parse_jest(self, result: TestResult, output: str):
208
+ """解析 Jest/Vitest 輸出"""
209
+ # 嘗試解析 JSON 輸出
210
+ try:
211
+ # 找到 JSON 部分
212
+ json_match = re.search(r'\{.*"numTotalTests".*\}', output, re.DOTALL)
213
+ if json_match:
214
+ data = json.loads(json_match.group())
215
+ result.passed = data.get('numPassedTests', 0)
216
+ result.failed = data.get('numFailedTests', 0)
217
+ result.skipped = data.get('numPendingTests', 0)
218
+ return
219
+ except Exception:
220
+ pass
221
+
222
+ # 備援:用正則解析
223
+ match = re.search(r'Tests:\s+(\d+) passed', output)
224
+ if match:
225
+ result.passed = int(match.group(1))
226
+
227
+ match = re.search(r'(\d+) failed', output)
228
+ if match:
229
+ result.failed = int(match.group(1))
230
+
231
+ def _parse_karma(self, result: TestResult, output: str):
232
+ """解析 Karma 輸出"""
233
+ match = re.search(r'Executed (\d+) of (\d+)', output)
234
+ if match:
235
+ executed = int(match.group(1))
236
+ total = int(match.group(2))
237
+
238
+ if 'SUCCESS' in output:
239
+ result.passed = executed
240
+ else:
241
+ match = re.search(r'(\d+) FAILED', output)
242
+ if match:
243
+ result.failed = int(match.group(1))
244
+ result.passed = executed - result.failed
245
+
246
+
247
+ def render_test_result(result: TestResult, project_name: str):
248
+ """渲染測試結果"""
249
+
250
+ # 標題
251
+ status_color = "green" if result.success else "red"
252
+ status_icon = "✓" if result.success else "✗"
253
+
254
+ title = Text()
255
+ title.append(f"\n {project_name} ", style="bold white")
256
+ title.append("測試報告\n", style="dim")
257
+ console.print(Panel(title, border_style="cyan"))
258
+
259
+ # 狀態
260
+ console.print(f" [{status_color}]{status_icon}[/{status_color}] ", end="")
261
+ console.print(f"[bold {status_color}]{'PASSED' if result.success else 'FAILED'}[/bold {status_color}]")
262
+ console.print(f" [dim]Framework:[/dim] {result.framework}")
263
+ if result.duration:
264
+ console.print(f" [dim]Duration:[/dim] {result.duration:.2f}s")
265
+ console.print()
266
+
267
+ # 統計
268
+ total = result.passed + result.failed + result.skipped
269
+ if total > 0:
270
+ pass_rate = (result.passed / total) * 100
271
+
272
+ # 進度條
273
+ bar_width = 40
274
+ passed_width = int((result.passed / total) * bar_width)
275
+ failed_width = int((result.failed / total) * bar_width)
276
+ skipped_width = bar_width - passed_width - failed_width
277
+
278
+ bar = Text()
279
+ bar.append(" ")
280
+ bar.append("█" * passed_width, style="green")
281
+ bar.append("█" * failed_width, style="red")
282
+ bar.append("█" * skipped_width, style="yellow")
283
+ bar.append(f" {pass_rate:.1f}%", style="green" if pass_rate >= 80 else "yellow")
284
+ console.print(bar)
285
+ console.print()
286
+
287
+ # 數字統計
288
+ stats = Text()
289
+ stats.append(" ")
290
+ stats.append(f"{result.passed} passed", style="green")
291
+ stats.append(" | ")
292
+ stats.append(f"{result.failed} failed", style="red" if result.failed else "dim")
293
+ stats.append(" | ")
294
+ stats.append(f"{result.skipped} skipped", style="yellow" if result.skipped else "dim")
295
+ console.print(stats)
296
+
297
+ # 覆蓋率
298
+ if result.coverage > 0:
299
+ console.print()
300
+ cov_color = "green" if result.coverage >= 80 else "yellow" if result.coverage >= 60 else "red"
301
+ console.print(f" [dim]Coverage:[/dim] [{cov_color}]{result.coverage:.1f}%[/{cov_color}]")
302
+
303
+ # 失敗的測試
304
+ if result.failed_tests:
305
+ console.print()
306
+ console.print(" [bold red]Failed Tests:[/bold red]")
307
+ for test in result.failed_tests[:10]:
308
+ console.print(f" [red]•[/red] {test}")
309
+ if len(result.failed_tests) > 10:
310
+ console.print(f" [dim]... and {len(result.failed_tests) - 10} more[/dim]")
311
+
312
+ console.print()
313
+
314
+ return {
315
+ 'success': result.success,
316
+ 'passed': result.passed,
317
+ 'failed': result.failed,
318
+ 'skipped': result.skipped,
319
+ 'coverage': result.coverage
320
+ }
321
+
322
+
323
+ def run_test(project_path: str, coverage: bool = False, verbose: bool = False) -> dict:
324
+ """執行測試"""
325
+ runner = TestRunner(project_path)
326
+
327
+ with Progress(
328
+ SpinnerColumn(),
329
+ TextColumn("[progress.description]{task.description}"),
330
+ console=console
331
+ ) as progress:
332
+ task = progress.add_task("執行測試中...", total=None)
333
+ result = runner.run(with_coverage=coverage, verbose=verbose)
334
+ progress.update(task, description="[green]測試完成 ✓")
335
+
336
+ return render_test_result(result, runner.project_name)
337
+
338
+
339
+ def run_test_all(projects: List[str], coverage: bool = False) -> dict:
340
+ """多專案測試"""
341
+ results = []
342
+
343
+ console.print(Panel(
344
+ Text("\n 專案測試總覽\n", style="bold white"),
345
+ border_style="cyan"
346
+ ))
347
+
348
+ table = Table(show_header=True, header_style="bold cyan")
349
+ table.add_column("專案", style="white")
350
+ table.add_column("狀態", justify="center")
351
+ table.add_column("通過", justify="right", style="green")
352
+ table.add_column("失敗", justify="right", style="red")
353
+ table.add_column("覆蓋率", justify="right")
354
+ table.add_column("Framework", style="dim")
355
+
356
+ for project in projects:
357
+ try:
358
+ runner = TestRunner(project)
359
+ framework, _ = runner.detect_framework()
360
+
361
+ if not framework:
362
+ table.add_row(
363
+ Path(project).name,
364
+ "[dim]-[/dim]",
365
+ "-",
366
+ "-",
367
+ "-",
368
+ "[dim]無測試[/dim]"
369
+ )
370
+ continue
371
+
372
+ result = runner.run(with_coverage=coverage)
373
+
374
+ status = "[green]PASS[/green]" if result.success else "[red]FAIL[/red]"
375
+ cov_display = f"{result.coverage:.0f}%" if result.coverage > 0 else "-"
376
+
377
+ table.add_row(
378
+ Path(project).name,
379
+ status,
380
+ str(result.passed),
381
+ str(result.failed),
382
+ cov_display,
383
+ result.framework
384
+ )
385
+
386
+ results.append({
387
+ 'project': Path(project).name,
388
+ 'success': result.success,
389
+ 'passed': result.passed,
390
+ 'failed': result.failed
391
+ })
392
+
393
+ except Exception as e:
394
+ table.add_row(
395
+ Path(project).name,
396
+ "[red]ERROR[/red]",
397
+ "-",
398
+ "-",
399
+ "-",
400
+ f"[red]{str(e)[:20]}[/red]"
401
+ )
402
+
403
+ console.print(table)
404
+
405
+ # 總結
406
+ total_passed = sum(r['passed'] for r in results)
407
+ total_failed = sum(r['failed'] for r in results)
408
+ all_success = all(r['success'] for r in results)
409
+
410
+ console.print()
411
+ if all_success:
412
+ console.print(f" [green]所有專案測試通過[/green] ({total_passed} tests)")
413
+ else:
414
+ console.print(f" [red]部分專案測試失敗[/red] ({total_failed} failures)")
415
+
416
+ return {'projects': results, 'all_success': all_success}
@@ -0,0 +1,157 @@
1
+ """
2
+ 驗證工具集
3
+
4
+ 新架構:依專案類型自動選擇驗證器
5
+
6
+ - common: 通用驗證(安全、品質)- 所有專案
7
+ - frontend/vite: Vite + Shoelace
8
+ - frontend/angular: Angular + PrimeNG
9
+ - frontend/gas: GAS (Google Apps Script) + Vue 3 + Shoelace/DaisyUI
10
+ - backend/nodejs: Node.js API
11
+ - backend/python: Python 後端/AI
12
+ """
13
+
14
+ from pathlib import Path
15
+ from .detector import ProjectDetector
16
+ from .common import SecurityValidator, QualityValidator, SpecValidator
17
+ from .frontend import ViteValidator, AngularValidator, GasValidator
18
+ from .backend import NodejsValidator, PythonValidator
19
+
20
+ # 保留舊的匯入(向後相容)
21
+ from .migration import MigrationValidator
22
+ from .security import SecurityValidator as LegacySecurityValidator
23
+ from .performance import PerformanceValidator
24
+ from .code_quality import CodeQualityValidator
25
+
26
+ __all__ = [
27
+ # 新架構
28
+ 'ProjectDetector',
29
+ 'SecurityValidator',
30
+ 'QualityValidator',
31
+ 'SpecValidator',
32
+ 'ViteValidator',
33
+ 'AngularValidator',
34
+ 'GasValidator',
35
+ 'NodejsValidator',
36
+ 'PythonValidator',
37
+ # 舊架構(向後相容)
38
+ 'MigrationValidator',
39
+ 'PerformanceValidator',
40
+ 'CodeQualityValidator',
41
+ # 函數
42
+ 'run_validation',
43
+ 'run_smart_validation',
44
+ ]
45
+
46
+
47
+ def run_smart_validation(projects, output=None):
48
+ """智慧驗證:自動偵測專案類型並執行對應驗證器"""
49
+ results = []
50
+
51
+ for project in projects:
52
+ project_path = Path(project)
53
+ project_name = project_path.name
54
+
55
+ result = {
56
+ 'project': project_name,
57
+ 'path': str(project_path),
58
+ 'passed': True,
59
+ 'errors': [],
60
+ 'warnings': [],
61
+ 'checks': {},
62
+ 'project_type': None
63
+ }
64
+
65
+ # 偵測專案類型
66
+ detector = ProjectDetector(project_path)
67
+ project_info = detector.detect()
68
+ result['project_type'] = project_info
69
+
70
+ validators = []
71
+
72
+ # 通用驗證器(所有專案都跑)
73
+ validators.append(SecurityValidator(project_path))
74
+ validators.append(QualityValidator(project_path))
75
+
76
+ # OpenSpec 驗證器(如果有 openspec/ 目錄)
77
+ openspec_dir = project_path / 'openspec'
78
+ if openspec_dir.exists():
79
+ validators.append(SpecValidator(project_path))
80
+
81
+ # 前端驗證器
82
+ if project_info['frontend'] == 'gas':
83
+ validators.append(GasValidator(project_path))
84
+ elif project_info['frontend'] == 'angular':
85
+ validators.append(AngularValidator(project_path))
86
+ elif project_info['frontend'] in ['vite', 'vanilla']:
87
+ validators.append(ViteValidator(project_path))
88
+
89
+ # 後端驗證器
90
+ if project_info['backend'] == 'nodejs':
91
+ validators.append(NodejsValidator(project_path))
92
+ elif project_info['backend'] == 'python':
93
+ validators.append(PythonValidator(project_path))
94
+
95
+ # 執行驗證
96
+ for validator in validators:
97
+ try:
98
+ check_result = validator.run()
99
+ result['checks'][validator.name] = check_result
100
+ if not check_result.get('passed', True):
101
+ result['passed'] = False
102
+ result['errors'].extend(check_result.get('errors', []))
103
+ result['warnings'].extend(check_result.get('warnings', []))
104
+ except Exception as e:
105
+ result['checks'][validator.name] = {
106
+ 'error': str(e),
107
+ 'passed': False
108
+ }
109
+ result['errors'].append(f'{validator.name} 驗證器錯誤: {e}')
110
+
111
+ results.append(result)
112
+
113
+ return results
114
+
115
+
116
+ def run_validation(projects, checks='all', output=None):
117
+ """執行驗證(向後相容模式)
118
+
119
+ 如果 checks='smart',使用新的智慧驗證
120
+ 否則使用舊的驗證邏輯
121
+ """
122
+ if checks == 'smart':
123
+ return run_smart_validation(projects, output)
124
+
125
+ results = []
126
+
127
+ for project in projects:
128
+ result = {
129
+ 'project': project.split('/')[-1] if '/' in project else project,
130
+ 'path': project,
131
+ 'passed': True,
132
+ 'errors': [],
133
+ 'warnings': [],
134
+ 'checks': {}
135
+ }
136
+
137
+ validators = []
138
+ if checks in ('all', 'migration'):
139
+ validators.append(MigrationValidator(project))
140
+ if checks in ('all', 'security'):
141
+ validators.append(LegacySecurityValidator(project))
142
+ if checks in ('all', 'performance'):
143
+ validators.append(PerformanceValidator(project))
144
+ if checks in ('all', 'code_quality'):
145
+ validators.append(CodeQualityValidator(project))
146
+
147
+ for validator in validators:
148
+ check_result = validator.run()
149
+ result['checks'][validator.name] = check_result
150
+ if not check_result.get('passed', True):
151
+ result['passed'] = False
152
+ result['errors'].extend(check_result.get('errors', []))
153
+ result['warnings'].extend(check_result.get('warnings', []))
154
+
155
+ results.append(result)
156
+
157
+ return results
@@ -0,0 +1,12 @@
1
+ """
2
+ 後端驗證器
3
+
4
+ 支援:
5
+ - Node.js (Vercel Serverless, Express, NestJS)
6
+ - Python (FastAPI, Flask, Django, Streamlit)
7
+ """
8
+
9
+ from .nodejs import NodejsValidator
10
+ from .python import PythonValidator
11
+
12
+ __all__ = ['NodejsValidator', 'PythonValidator']