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.
- dash_devtools/__init__.py +8 -0
- dash_devtools/__main__.py +11 -0
- dash_devtools/ai_engine.py +441 -0
- dash_devtools/browser.py +541 -0
- dash_devtools/cli.py +1452 -0
- dash_devtools/database.py +338 -0
- dash_devtools/dbdiagram.py +183 -0
- dash_devtools/e2e.py +329 -0
- dash_devtools/fixers/__init__.py +57 -0
- dash_devtools/fixers/migration_fixer.py +115 -0
- dash_devtools/fixers/ux_fixer.py +106 -0
- dash_devtools/fixers/version_bumper.py +115 -0
- dash_devtools/gas_mes_test.py +1241 -0
- dash_devtools/generators/__init__.py +84 -0
- dash_devtools/health.py +476 -0
- dash_devtools/hooks/__init__.py +250 -0
- dash_devtools/hooks/pre_commit.py +161 -0
- dash_devtools/hooks/pre_push.py +275 -0
- dash_devtools/init_test.py +352 -0
- dash_devtools/markdown_report.py +309 -0
- dash_devtools/migrators/__init__.py +21 -0
- dash_devtools/perf.py +321 -0
- dash_devtools/report.py +667 -0
- dash_devtools/reporters/__init__.py +11 -0
- dash_devtools/spec.py +230 -0
- dash_devtools/stats.py +355 -0
- dash_devtools/test_suite.py +690 -0
- dash_devtools/testing.py +416 -0
- dash_devtools/validators/__init__.py +157 -0
- dash_devtools/validators/backend/__init__.py +12 -0
- dash_devtools/validators/backend/nodejs.py +245 -0
- dash_devtools/validators/backend/python.py +439 -0
- dash_devtools/validators/code_quality.py +243 -0
- dash_devtools/validators/common/__init__.py +11 -0
- dash_devtools/validators/common/quality.py +319 -0
- dash_devtools/validators/common/security.py +270 -0
- dash_devtools/validators/common/spec.py +273 -0
- dash_devtools/validators/detector.py +394 -0
- dash_devtools/validators/frontend/__init__.py +14 -0
- dash_devtools/validators/frontend/angular.py +245 -0
- dash_devtools/validators/frontend/gas.py +310 -0
- dash_devtools/validators/frontend/vite.py +539 -0
- dash_devtools/validators/migration.py +292 -0
- dash_devtools/validators/performance.py +167 -0
- dash_devtools/validators/security.py +205 -0
- dash_devtools/vision/__init__.py +368 -0
- dash_devtools/watch.py +266 -0
- dash_devtools/word_report.py +690 -0
- dash_devtools-1.0.0.dist-info/METADATA +834 -0
- dash_devtools-1.0.0.dist-info/RECORD +53 -0
- dash_devtools-1.0.0.dist-info/WHEEL +5 -0
- dash_devtools-1.0.0.dist-info/entry_points.txt +2 -0
- dash_devtools-1.0.0.dist-info/top_level.txt +1 -0
dash_devtools/testing.py
ADDED
|
@@ -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
|