super-dev 2.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.
- super_dev/__init__.py +11 -0
- super_dev/analyzer/__init__.py +34 -0
- super_dev/analyzer/analyzer.py +440 -0
- super_dev/analyzer/detectors.py +511 -0
- super_dev/analyzer/models.py +285 -0
- super_dev/cli.py +3257 -0
- super_dev/config/__init__.py +11 -0
- super_dev/config/frontend.py +557 -0
- super_dev/config/manager.py +281 -0
- super_dev/creators/__init__.py +26 -0
- super_dev/creators/creator.py +134 -0
- super_dev/creators/document_generator.py +2473 -0
- super_dev/creators/frontend_builder.py +371 -0
- super_dev/creators/implementation_builder.py +789 -0
- super_dev/creators/prompt_generator.py +289 -0
- super_dev/creators/requirement_parser.py +354 -0
- super_dev/creators/spec_builder.py +195 -0
- super_dev/deployers/__init__.py +20 -0
- super_dev/deployers/cicd.py +1269 -0
- super_dev/deployers/delivery.py +229 -0
- super_dev/deployers/migration.py +1032 -0
- super_dev/design/__init__.py +74 -0
- super_dev/design/aesthetics.py +530 -0
- super_dev/design/charts.py +396 -0
- super_dev/design/codegen.py +379 -0
- super_dev/design/engine.py +528 -0
- super_dev/design/generator.py +395 -0
- super_dev/design/landing.py +422 -0
- super_dev/design/tech_stack.py +524 -0
- super_dev/design/tokens.py +269 -0
- super_dev/design/ux_guide.py +391 -0
- super_dev/exceptions.py +119 -0
- super_dev/experts/__init__.py +19 -0
- super_dev/experts/service.py +161 -0
- super_dev/integrations/__init__.py +7 -0
- super_dev/integrations/manager.py +264 -0
- super_dev/orchestrator/__init__.py +12 -0
- super_dev/orchestrator/engine.py +958 -0
- super_dev/orchestrator/experts.py +423 -0
- super_dev/orchestrator/knowledge.py +352 -0
- super_dev/orchestrator/quality.py +356 -0
- super_dev/reviewers/__init__.py +17 -0
- super_dev/reviewers/code_review.py +471 -0
- super_dev/reviewers/quality_gate.py +964 -0
- super_dev/reviewers/redteam.py +881 -0
- super_dev/skills/__init__.py +7 -0
- super_dev/skills/manager.py +307 -0
- super_dev/specs/__init__.py +44 -0
- super_dev/specs/generator.py +264 -0
- super_dev/specs/manager.py +428 -0
- super_dev/specs/models.py +348 -0
- super_dev/specs/validator.py +415 -0
- super_dev/utils/__init__.py +11 -0
- super_dev/utils/logger.py +133 -0
- super_dev/web/api.py +1402 -0
- super_dev-2.0.0.dist-info/METADATA +252 -0
- super_dev-2.0.0.dist-info/RECORD +61 -0
- super_dev-2.0.0.dist-info/WHEEL +5 -0
- super_dev-2.0.0.dist-info/entry_points.txt +2 -0
- super_dev-2.0.0.dist-info/licenses/LICENSE +21 -0
- super_dev-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
"""
|
|
2
|
+
质量门禁检查器 - 确保交付物达到质量标准
|
|
3
|
+
|
|
4
|
+
开发:Excellent(11964948@qq.com)
|
|
5
|
+
功能:多维度质量评分和门禁检查
|
|
6
|
+
作用:按场景阈值(或自定义阈值)评估是否通过质量门禁
|
|
7
|
+
创建时间:2025-12-30
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess # nosec B404
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from defusedxml import ElementTree
|
|
19
|
+
|
|
20
|
+
from .redteam import RedTeamReport
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CheckStatus(Enum):
|
|
24
|
+
"""检查状态"""
|
|
25
|
+
PASSED = "passed"
|
|
26
|
+
FAILED = "failed"
|
|
27
|
+
WARNING = "warning"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class QualityCheck:
|
|
32
|
+
"""质量检查项"""
|
|
33
|
+
name: str
|
|
34
|
+
category: str # documentation, security, performance, testing, code_quality
|
|
35
|
+
description: str
|
|
36
|
+
status: CheckStatus
|
|
37
|
+
score: int # 0-100
|
|
38
|
+
weight: float = 1.0 # 权重,用于计算加权总分
|
|
39
|
+
details: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class QualityGateResult:
|
|
44
|
+
"""质量门禁结果"""
|
|
45
|
+
passed: bool
|
|
46
|
+
total_score: int
|
|
47
|
+
weighted_score: float
|
|
48
|
+
checks: list[QualityCheck] = field(default_factory=list)
|
|
49
|
+
critical_failures: list[str] = field(default_factory=list)
|
|
50
|
+
recommendations: list[str] = field(default_factory=list)
|
|
51
|
+
scenario: str = "1-N+1" # 场景类型: "0-1" 或 "1-N+1"
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def passed_checks(self) -> list[QualityCheck]:
|
|
55
|
+
return [c for c in self.checks if c.status == CheckStatus.PASSED]
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def failed_checks(self) -> list[QualityCheck]:
|
|
59
|
+
return [c for c in self.checks if c.status == CheckStatus.FAILED]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def warning_checks(self) -> list[QualityCheck]:
|
|
63
|
+
return [c for c in self.checks if c.status == CheckStatus.WARNING]
|
|
64
|
+
|
|
65
|
+
def to_markdown(self) -> str:
|
|
66
|
+
"""生成 Markdown 报告"""
|
|
67
|
+
status_icon = "通过" if self.passed else "未通过"
|
|
68
|
+
status_color = "green" if self.passed else "red"
|
|
69
|
+
|
|
70
|
+
lines = [
|
|
71
|
+
"# 质量门禁报告",
|
|
72
|
+
"",
|
|
73
|
+
f"**场景**: {self.scenario} ({'0-1 新建项目' if self.scenario == '0-1' else '1-N+1 增量开发'})",
|
|
74
|
+
f"**状态**: <span style='color:{status_color}'>{status_icon}</span>",
|
|
75
|
+
f"**总分**: {self.total_score}/100",
|
|
76
|
+
f"**加权分**: {self.weighted_score:.1f}/100",
|
|
77
|
+
"",
|
|
78
|
+
"---",
|
|
79
|
+
"",
|
|
80
|
+
"## 检查结果摘要",
|
|
81
|
+
"",
|
|
82
|
+
f"- 通过: {len(self.passed_checks)} 项",
|
|
83
|
+
f"- 警告: {len(self.warning_checks)} 项",
|
|
84
|
+
f"- 失败: {len(self.failed_checks)} 项",
|
|
85
|
+
"",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
if self.critical_failures:
|
|
89
|
+
lines.extend([
|
|
90
|
+
"## 关键失败项",
|
|
91
|
+
"",
|
|
92
|
+
])
|
|
93
|
+
for failure in self.critical_failures:
|
|
94
|
+
lines.append(f"- {failure}")
|
|
95
|
+
lines.append("")
|
|
96
|
+
|
|
97
|
+
# 按类别分组展示
|
|
98
|
+
categories: dict[str, list[QualityCheck]] = {}
|
|
99
|
+
for check in self.checks:
|
|
100
|
+
if check.category not in categories:
|
|
101
|
+
categories[check.category] = []
|
|
102
|
+
categories[check.category].append(check)
|
|
103
|
+
|
|
104
|
+
lines.extend([
|
|
105
|
+
"## 详细检查结果",
|
|
106
|
+
"",
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
for category, checks in categories.items():
|
|
110
|
+
lines.extend([
|
|
111
|
+
f"### {category}",
|
|
112
|
+
"",
|
|
113
|
+
"| 检查项 | 状态 | 得分 | 说明 |",
|
|
114
|
+
"|:---|:---:|:---:|:---|",
|
|
115
|
+
])
|
|
116
|
+
|
|
117
|
+
for check in checks:
|
|
118
|
+
status_icon = "✓" if check.status == CheckStatus.PASSED else "⚠" if check.status == CheckStatus.WARNING else "✗"
|
|
119
|
+
lines.append(
|
|
120
|
+
f"| {check.name} | {status_icon} | {check.score}/100 | {check.description} |"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
# 改进建议
|
|
126
|
+
if self.recommendations:
|
|
127
|
+
lines.extend([
|
|
128
|
+
"## 改进建议",
|
|
129
|
+
"",
|
|
130
|
+
])
|
|
131
|
+
for idx, rec in enumerate(self.recommendations, 1):
|
|
132
|
+
lines.append(f"{idx}. {rec}")
|
|
133
|
+
lines.append("")
|
|
134
|
+
|
|
135
|
+
# 下一步行动
|
|
136
|
+
lines.extend([
|
|
137
|
+
"---",
|
|
138
|
+
"",
|
|
139
|
+
"## 下一步行动",
|
|
140
|
+
"",
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
if self.passed:
|
|
144
|
+
lines.extend([
|
|
145
|
+
"[通过] 质量门禁已通过,可以继续下一步:",
|
|
146
|
+
"",
|
|
147
|
+
"1. 开始编码实现",
|
|
148
|
+
"2. 设置 CI/CD 流水线",
|
|
149
|
+
"3. 部署到测试环境",
|
|
150
|
+
"",
|
|
151
|
+
])
|
|
152
|
+
else:
|
|
153
|
+
lines.extend([
|
|
154
|
+
"[未通过] 质量门禁未通过,请完成以下操作后重新检查:",
|
|
155
|
+
"",
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
failed_items = [f"- {c.description}" for c in self.failed_checks]
|
|
159
|
+
lines.extend(failed_items)
|
|
160
|
+
lines.extend([
|
|
161
|
+
"",
|
|
162
|
+
"修复后运行: `super-dev quality --type all`",
|
|
163
|
+
"",
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
return "\n".join(lines)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class QualityGateChecker:
|
|
170
|
+
"""质量门禁检查器"""
|
|
171
|
+
|
|
172
|
+
# 质量门禁阈值
|
|
173
|
+
PASS_THRESHOLD = 80
|
|
174
|
+
WARNING_THRESHOLD = 60
|
|
175
|
+
# 0-1 场景与增量场景统一使用 80+ 标准
|
|
176
|
+
PASS_THRESHOLD_ZERO_TO_ONE = 80
|
|
177
|
+
|
|
178
|
+
# 检查项配置
|
|
179
|
+
CHECKS_CONFIG = {
|
|
180
|
+
"documentation": {
|
|
181
|
+
"weight": 1.0,
|
|
182
|
+
"required": True,
|
|
183
|
+
},
|
|
184
|
+
"security": {
|
|
185
|
+
"weight": 1.5, # 安全更重要
|
|
186
|
+
"required": True,
|
|
187
|
+
},
|
|
188
|
+
"performance": {
|
|
189
|
+
"weight": 1.2,
|
|
190
|
+
"required": True,
|
|
191
|
+
},
|
|
192
|
+
"testing": {
|
|
193
|
+
"weight": 1.3,
|
|
194
|
+
"required": True,
|
|
195
|
+
},
|
|
196
|
+
"code_quality": {
|
|
197
|
+
"weight": 1.0,
|
|
198
|
+
"required": False,
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def __init__(
|
|
203
|
+
self,
|
|
204
|
+
project_dir: Path,
|
|
205
|
+
name: str,
|
|
206
|
+
tech_stack: dict,
|
|
207
|
+
scenario_override: str | None = None,
|
|
208
|
+
threshold_override: int | None = None,
|
|
209
|
+
):
|
|
210
|
+
self.project_dir = Path(project_dir).resolve()
|
|
211
|
+
self.name = name
|
|
212
|
+
self.tech_stack = tech_stack
|
|
213
|
+
self.threshold_override = threshold_override
|
|
214
|
+
if scenario_override in {"0-1", "1-N+1"}:
|
|
215
|
+
self.is_zero_to_one = scenario_override == "0-1"
|
|
216
|
+
else:
|
|
217
|
+
self.is_zero_to_one = self._detect_zero_to_one_scenario()
|
|
218
|
+
|
|
219
|
+
def _detect_zero_to_one_scenario(self) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
检测是否为 0-1 场景(空项目/新建项目)
|
|
222
|
+
|
|
223
|
+
0-1 场景特征:
|
|
224
|
+
- 没有源代码目录(src/, lib/, app/, server/, client/ 等)
|
|
225
|
+
- 没有配置文件(package.json, requirements.txt, go.mod 等)
|
|
226
|
+
- 只有 output/ 目录(刚生成的文档)
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
bool: True 表示 0-1 场景,False 表示 1-N+1 场景
|
|
230
|
+
"""
|
|
231
|
+
# 检查常见的源代码目录
|
|
232
|
+
source_dirs = [
|
|
233
|
+
"src", "lib", "app", "server", "client",
|
|
234
|
+
"backend", "frontend", "api", "handlers",
|
|
235
|
+
"models", "views", "controllers", "services"
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
has_source_code = any(
|
|
239
|
+
(self.project_dir / d).exists()
|
|
240
|
+
for d in source_dirs
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# 检查是否有项目配置文件(表明这不是空项目)
|
|
244
|
+
config_files = [
|
|
245
|
+
"package.json", "requirements.txt", "go.mod",
|
|
246
|
+
"Cargo.toml", "pom.xml", "build.gradle"
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
has_project_config = any(
|
|
250
|
+
(self.project_dir / f).exists()
|
|
251
|
+
for f in config_files
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# 如果有源代码或有项目配置,说明不是 0-1 场景
|
|
255
|
+
return not (has_source_code or has_project_config)
|
|
256
|
+
|
|
257
|
+
def check(self, redteam_report: Optional["RedTeamReport"] = None) -> QualityGateResult:
|
|
258
|
+
"""执行质量门禁检查"""
|
|
259
|
+
checks: list[QualityCheck] = []
|
|
260
|
+
|
|
261
|
+
# 1. 文档质量检查
|
|
262
|
+
checks.extend(self._check_documentation())
|
|
263
|
+
|
|
264
|
+
# 2. 安全检查 (基于红队报告)
|
|
265
|
+
checks.extend(self._check_security(redteam_report))
|
|
266
|
+
|
|
267
|
+
# 3. 性能检查 (基于红队报告)
|
|
268
|
+
checks.extend(self._check_performance(redteam_report))
|
|
269
|
+
|
|
270
|
+
# 4. 测试检查
|
|
271
|
+
checks.extend(self._check_testing())
|
|
272
|
+
|
|
273
|
+
# 5. 代码质量检查
|
|
274
|
+
checks.extend(self._check_code_quality())
|
|
275
|
+
|
|
276
|
+
# 计算总分和加权分
|
|
277
|
+
total_score = self._calculate_total_score(checks)
|
|
278
|
+
weighted_score = self._calculate_weighted_score(checks)
|
|
279
|
+
|
|
280
|
+
# 根据场景选择阈值
|
|
281
|
+
threshold = (
|
|
282
|
+
self.threshold_override
|
|
283
|
+
if self.threshold_override is not None
|
|
284
|
+
else (self.PASS_THRESHOLD_ZERO_TO_ONE if self.is_zero_to_one else self.PASS_THRESHOLD)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# 收集关键失败项
|
|
288
|
+
critical_failures = []
|
|
289
|
+
for check in checks:
|
|
290
|
+
config = self.CHECKS_CONFIG.get(check.category, {})
|
|
291
|
+
if config.get("required", False) and check.status == CheckStatus.FAILED:
|
|
292
|
+
critical_failures.append(f"[{check.category}] {check.description}")
|
|
293
|
+
|
|
294
|
+
# 检查是否通过:必须达到阈值,且必检项不能失败
|
|
295
|
+
passed = total_score >= threshold and not critical_failures
|
|
296
|
+
|
|
297
|
+
# 生成改进建议
|
|
298
|
+
recommendations = self._generate_recommendations(checks)
|
|
299
|
+
|
|
300
|
+
# 确定场景类型
|
|
301
|
+
scenario = "0-1" if self.is_zero_to_one else "1-N+1"
|
|
302
|
+
|
|
303
|
+
return QualityGateResult(
|
|
304
|
+
passed=passed,
|
|
305
|
+
total_score=total_score,
|
|
306
|
+
weighted_score=weighted_score,
|
|
307
|
+
checks=checks,
|
|
308
|
+
critical_failures=critical_failures,
|
|
309
|
+
recommendations=recommendations,
|
|
310
|
+
scenario=scenario,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def _check_documentation(self) -> list[QualityCheck]:
|
|
314
|
+
"""检查文档质量"""
|
|
315
|
+
checks = []
|
|
316
|
+
|
|
317
|
+
# 检查 PRD 是否存在
|
|
318
|
+
prd_path = self.project_dir / "output" / f"{self.name}-prd.md"
|
|
319
|
+
if prd_path.exists():
|
|
320
|
+
content = prd_path.read_text(encoding="utf-8")
|
|
321
|
+
# 简单检查文档完整性
|
|
322
|
+
has_vision = "产品愿景" in content or "vision" in content.lower()
|
|
323
|
+
has_features = "功能需求" in content or "features" in content.lower()
|
|
324
|
+
has_acceptance = "验收标准" in content or "acceptance" in content.lower()
|
|
325
|
+
|
|
326
|
+
score = 100 if has_vision and has_features and has_acceptance else 70
|
|
327
|
+
status = CheckStatus.PASSED if score >= 80 else CheckStatus.WARNING
|
|
328
|
+
|
|
329
|
+
checks.append(QualityCheck(
|
|
330
|
+
name="PRD 文档",
|
|
331
|
+
category="documentation",
|
|
332
|
+
description="产品需求文档完整性",
|
|
333
|
+
status=status,
|
|
334
|
+
score=score,
|
|
335
|
+
weight=self.CHECKS_CONFIG["documentation"]["weight"],
|
|
336
|
+
details="包含产品愿景、功能需求和验收标准" if status == CheckStatus.PASSED else "文档内容不完整",
|
|
337
|
+
))
|
|
338
|
+
else:
|
|
339
|
+
checks.append(QualityCheck(
|
|
340
|
+
name="PRD 文档",
|
|
341
|
+
category="documentation",
|
|
342
|
+
description="产品需求文档存在性",
|
|
343
|
+
status=CheckStatus.FAILED,
|
|
344
|
+
score=0,
|
|
345
|
+
weight=self.CHECKS_CONFIG["documentation"]["weight"],
|
|
346
|
+
details="PRD 文档不存在",
|
|
347
|
+
))
|
|
348
|
+
|
|
349
|
+
# 检查架构文档是否存在
|
|
350
|
+
arch_path = self.project_dir / "output" / f"{self.name}-architecture.md"
|
|
351
|
+
if arch_path.exists():
|
|
352
|
+
content = arch_path.read_text(encoding="utf-8")
|
|
353
|
+
has_tech_stack = "技术栈" in content or "tech stack" in content.lower()
|
|
354
|
+
has_database = "数据库" in content or "database" in content.lower()
|
|
355
|
+
has_api = "API" in content
|
|
356
|
+
|
|
357
|
+
score = 100 if has_tech_stack and has_database and has_api else 70
|
|
358
|
+
status = CheckStatus.PASSED if score >= 80 else CheckStatus.WARNING
|
|
359
|
+
|
|
360
|
+
checks.append(QualityCheck(
|
|
361
|
+
name="架构文档",
|
|
362
|
+
category="documentation",
|
|
363
|
+
description="架构设计文档完整性",
|
|
364
|
+
status=status,
|
|
365
|
+
score=score,
|
|
366
|
+
weight=self.CHECKS_CONFIG["documentation"]["weight"],
|
|
367
|
+
details="包含技术栈、数据库设计和 API 设计" if status == CheckStatus.PASSED else "文档内容不完整",
|
|
368
|
+
))
|
|
369
|
+
else:
|
|
370
|
+
checks.append(QualityCheck(
|
|
371
|
+
name="架构文档",
|
|
372
|
+
category="documentation",
|
|
373
|
+
description="架构设计文档存在性",
|
|
374
|
+
status=CheckStatus.FAILED,
|
|
375
|
+
score=0,
|
|
376
|
+
weight=self.CHECKS_CONFIG["documentation"]["weight"],
|
|
377
|
+
details="架构文档不存在",
|
|
378
|
+
))
|
|
379
|
+
|
|
380
|
+
# 检查 UI/UX 文档是否存在
|
|
381
|
+
uiux_path = self.project_dir / "output" / f"{self.name}-uiux.md"
|
|
382
|
+
if uiux_path.exists():
|
|
383
|
+
checks.append(QualityCheck(
|
|
384
|
+
name="UI/UX 文档",
|
|
385
|
+
category="documentation",
|
|
386
|
+
description="UI/UX 设计文档存在性",
|
|
387
|
+
status=CheckStatus.PASSED,
|
|
388
|
+
score=100,
|
|
389
|
+
weight=self.CHECKS_CONFIG["documentation"]["weight"],
|
|
390
|
+
details="UI/UX 文档已创建",
|
|
391
|
+
))
|
|
392
|
+
else:
|
|
393
|
+
checks.append(QualityCheck(
|
|
394
|
+
name="UI/UX 文档",
|
|
395
|
+
category="documentation",
|
|
396
|
+
description="UI/UX 设计文档存在性",
|
|
397
|
+
status=CheckStatus.WARNING,
|
|
398
|
+
score=50,
|
|
399
|
+
weight=self.CHECKS_CONFIG["documentation"]["weight"],
|
|
400
|
+
details="UI/UX 文档不存在(可选)",
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
return checks
|
|
404
|
+
|
|
405
|
+
def _check_security(self, redteam_report: RedTeamReport | None) -> list[QualityCheck]:
|
|
406
|
+
"""检查安全性"""
|
|
407
|
+
checks: list[QualityCheck] = []
|
|
408
|
+
|
|
409
|
+
if redteam_report:
|
|
410
|
+
critical_count = sum(1 for i in redteam_report.security_issues if i.severity == "critical")
|
|
411
|
+
high_count = sum(1 for i in redteam_report.security_issues if i.severity == "high")
|
|
412
|
+
|
|
413
|
+
if critical_count > 0:
|
|
414
|
+
score = max(0, 100 - critical_count * 30)
|
|
415
|
+
status = CheckStatus.FAILED
|
|
416
|
+
elif high_count > 2:
|
|
417
|
+
score = max(0, 100 - high_count * 15)
|
|
418
|
+
status = CheckStatus.WARNING
|
|
419
|
+
else:
|
|
420
|
+
score = 100
|
|
421
|
+
status = CheckStatus.PASSED
|
|
422
|
+
|
|
423
|
+
checks.append(QualityCheck(
|
|
424
|
+
name="安全审查",
|
|
425
|
+
category="security",
|
|
426
|
+
description=f"安全检查 ({critical_count} critical, {high_count} high)",
|
|
427
|
+
status=status,
|
|
428
|
+
score=score,
|
|
429
|
+
weight=self.CHECKS_CONFIG["security"]["weight"],
|
|
430
|
+
details=f"发现 {critical_count} 个严重问题和 {high_count} 个高危问题" if critical_count + high_count > 0 else "未发现严重安全问题",
|
|
431
|
+
))
|
|
432
|
+
else:
|
|
433
|
+
# 未进行红队审查,给警告
|
|
434
|
+
checks.append(QualityCheck(
|
|
435
|
+
name="安全审查",
|
|
436
|
+
category="security",
|
|
437
|
+
description="安全检查状态",
|
|
438
|
+
status=CheckStatus.WARNING,
|
|
439
|
+
score=50,
|
|
440
|
+
weight=self.CHECKS_CONFIG["security"]["weight"],
|
|
441
|
+
details="未进行红队安全审查",
|
|
442
|
+
))
|
|
443
|
+
|
|
444
|
+
return checks
|
|
445
|
+
|
|
446
|
+
def _check_performance(self, redteam_report: RedTeamReport | None) -> list[QualityCheck]:
|
|
447
|
+
"""检查性能"""
|
|
448
|
+
checks: list[QualityCheck] = []
|
|
449
|
+
|
|
450
|
+
if redteam_report:
|
|
451
|
+
critical_count = sum(1 for i in redteam_report.performance_issues if i.severity == "critical")
|
|
452
|
+
high_count = sum(1 for i in redteam_report.performance_issues if i.severity == "high")
|
|
453
|
+
|
|
454
|
+
if critical_count > 0:
|
|
455
|
+
score = max(0, 100 - critical_count * 25)
|
|
456
|
+
status = CheckStatus.FAILED
|
|
457
|
+
elif high_count > 2:
|
|
458
|
+
score = max(0, 100 - high_count * 10)
|
|
459
|
+
status = CheckStatus.WARNING
|
|
460
|
+
else:
|
|
461
|
+
score = 100
|
|
462
|
+
status = CheckStatus.PASSED
|
|
463
|
+
|
|
464
|
+
checks.append(QualityCheck(
|
|
465
|
+
name="性能审查",
|
|
466
|
+
category="performance",
|
|
467
|
+
description=f"性能检查 ({critical_count} critical, {high_count} high)",
|
|
468
|
+
status=status,
|
|
469
|
+
score=score,
|
|
470
|
+
weight=self.CHECKS_CONFIG["performance"]["weight"],
|
|
471
|
+
details=f"发现 {critical_count} 个严重问题和 {high_count} 个高危问题" if critical_count + high_count > 0 else "未发现严重性能问题",
|
|
472
|
+
))
|
|
473
|
+
else:
|
|
474
|
+
checks.append(QualityCheck(
|
|
475
|
+
name="性能审查",
|
|
476
|
+
category="performance",
|
|
477
|
+
description="性能检查状态",
|
|
478
|
+
status=CheckStatus.WARNING,
|
|
479
|
+
score=50,
|
|
480
|
+
weight=self.CHECKS_CONFIG["performance"]["weight"],
|
|
481
|
+
details="未进行红队性能审查",
|
|
482
|
+
))
|
|
483
|
+
|
|
484
|
+
return checks
|
|
485
|
+
|
|
486
|
+
def _check_testing(self) -> list[QualityCheck]:
|
|
487
|
+
"""检查测试策略"""
|
|
488
|
+
checks: list[QualityCheck] = []
|
|
489
|
+
|
|
490
|
+
# 检查是否有测试配置
|
|
491
|
+
has_jest = self._has_js_test_script()
|
|
492
|
+
has_pytest = self._has_pytest_config()
|
|
493
|
+
|
|
494
|
+
if has_jest or has_pytest:
|
|
495
|
+
checks.append(QualityCheck(
|
|
496
|
+
name="测试框架",
|
|
497
|
+
category="testing",
|
|
498
|
+
description="测试框架配置",
|
|
499
|
+
status=CheckStatus.PASSED,
|
|
500
|
+
score=100,
|
|
501
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
502
|
+
details="测试框架已配置",
|
|
503
|
+
))
|
|
504
|
+
else:
|
|
505
|
+
checks.append(QualityCheck(
|
|
506
|
+
name="测试框架",
|
|
507
|
+
category="testing",
|
|
508
|
+
description="测试框架配置",
|
|
509
|
+
status=CheckStatus.WARNING,
|
|
510
|
+
score=50,
|
|
511
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
512
|
+
details="测试框架未配置",
|
|
513
|
+
))
|
|
514
|
+
|
|
515
|
+
python_tests = self._discover_python_tests()
|
|
516
|
+
js_test_targets = self._discover_js_test_targets()
|
|
517
|
+
|
|
518
|
+
# 真实测试执行检查(优先 Python)
|
|
519
|
+
if python_tests:
|
|
520
|
+
pytest_executable = shutil.which("pytest")
|
|
521
|
+
if pytest_executable:
|
|
522
|
+
result = self._run_command(
|
|
523
|
+
[pytest_executable, "-q", "--maxfail=1"],
|
|
524
|
+
timeout=180,
|
|
525
|
+
)
|
|
526
|
+
if result["timed_out"]:
|
|
527
|
+
checks.append(QualityCheck(
|
|
528
|
+
name="测试执行",
|
|
529
|
+
category="testing",
|
|
530
|
+
description="自动化测试执行结果",
|
|
531
|
+
status=CheckStatus.WARNING,
|
|
532
|
+
score=40,
|
|
533
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
534
|
+
details="pytest 执行超时,建议拆分测试或优化测试速度",
|
|
535
|
+
))
|
|
536
|
+
elif result["returncode"] == 0:
|
|
537
|
+
summary = self._extract_test_summary(str(result["stdout"]))
|
|
538
|
+
checks.append(QualityCheck(
|
|
539
|
+
name="测试执行",
|
|
540
|
+
category="testing",
|
|
541
|
+
description="自动化测试执行结果",
|
|
542
|
+
status=CheckStatus.PASSED,
|
|
543
|
+
score=100,
|
|
544
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
545
|
+
details=summary or "pytest 执行通过",
|
|
546
|
+
))
|
|
547
|
+
else:
|
|
548
|
+
summary = self._extract_test_summary(str(result["stdout"] or result["stderr"]))
|
|
549
|
+
checks.append(QualityCheck(
|
|
550
|
+
name="测试执行",
|
|
551
|
+
category="testing",
|
|
552
|
+
description="自动化测试执行结果",
|
|
553
|
+
status=CheckStatus.FAILED,
|
|
554
|
+
score=20,
|
|
555
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
556
|
+
details=summary or "pytest 执行失败",
|
|
557
|
+
))
|
|
558
|
+
else:
|
|
559
|
+
checks.append(QualityCheck(
|
|
560
|
+
name="测试执行",
|
|
561
|
+
category="testing",
|
|
562
|
+
description="自动化测试执行结果",
|
|
563
|
+
status=CheckStatus.WARNING,
|
|
564
|
+
score=40,
|
|
565
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
566
|
+
details="检测到 Python 测试,但未找到 pytest 可执行文件",
|
|
567
|
+
))
|
|
568
|
+
elif js_test_targets:
|
|
569
|
+
npm_executable = shutil.which("npm")
|
|
570
|
+
if not npm_executable:
|
|
571
|
+
checks.append(QualityCheck(
|
|
572
|
+
name="测试执行",
|
|
573
|
+
category="testing",
|
|
574
|
+
description="自动化测试执行结果",
|
|
575
|
+
status=CheckStatus.WARNING,
|
|
576
|
+
score=40,
|
|
577
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
578
|
+
details="检测到 JS 测试脚本,但未找到 npm 可执行文件",
|
|
579
|
+
))
|
|
580
|
+
else:
|
|
581
|
+
timed_out_targets: list[str] = []
|
|
582
|
+
failed_targets: list[str] = []
|
|
583
|
+
passed_targets: list[str] = []
|
|
584
|
+
for target in js_test_targets:
|
|
585
|
+
rel = "."
|
|
586
|
+
if target != self.project_dir:
|
|
587
|
+
rel = str(target.relative_to(self.project_dir))
|
|
588
|
+
result = self._run_command(
|
|
589
|
+
[
|
|
590
|
+
npm_executable,
|
|
591
|
+
"--prefix",
|
|
592
|
+
str(target),
|
|
593
|
+
"run",
|
|
594
|
+
"test",
|
|
595
|
+
"--if-present",
|
|
596
|
+
],
|
|
597
|
+
timeout=240,
|
|
598
|
+
)
|
|
599
|
+
if result["timed_out"]:
|
|
600
|
+
timed_out_targets.append(rel)
|
|
601
|
+
elif result["returncode"] == 0:
|
|
602
|
+
passed_targets.append(rel)
|
|
603
|
+
else:
|
|
604
|
+
failed_targets.append(rel)
|
|
605
|
+
|
|
606
|
+
if failed_targets:
|
|
607
|
+
checks.append(QualityCheck(
|
|
608
|
+
name="测试执行",
|
|
609
|
+
category="testing",
|
|
610
|
+
description="自动化测试执行结果",
|
|
611
|
+
status=CheckStatus.FAILED,
|
|
612
|
+
score=20,
|
|
613
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
614
|
+
details=f"JS 测试失败: {', '.join(failed_targets)}",
|
|
615
|
+
))
|
|
616
|
+
elif timed_out_targets:
|
|
617
|
+
checks.append(QualityCheck(
|
|
618
|
+
name="测试执行",
|
|
619
|
+
category="testing",
|
|
620
|
+
description="自动化测试执行结果",
|
|
621
|
+
status=CheckStatus.WARNING,
|
|
622
|
+
score=40,
|
|
623
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
624
|
+
details=f"JS 测试超时: {', '.join(timed_out_targets)}",
|
|
625
|
+
))
|
|
626
|
+
else:
|
|
627
|
+
checks.append(QualityCheck(
|
|
628
|
+
name="测试执行",
|
|
629
|
+
category="testing",
|
|
630
|
+
description="自动化测试执行结果",
|
|
631
|
+
status=CheckStatus.PASSED,
|
|
632
|
+
score=100,
|
|
633
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
634
|
+
details=f"JS 测试通过: {', '.join(passed_targets)}",
|
|
635
|
+
))
|
|
636
|
+
else:
|
|
637
|
+
warning_score = 70 if self.is_zero_to_one else 40
|
|
638
|
+
checks.append(QualityCheck(
|
|
639
|
+
name="测试执行",
|
|
640
|
+
category="testing",
|
|
641
|
+
description="自动化测试执行结果",
|
|
642
|
+
status=CheckStatus.WARNING,
|
|
643
|
+
score=warning_score,
|
|
644
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
645
|
+
details="未检测到可执行测试用例",
|
|
646
|
+
))
|
|
647
|
+
|
|
648
|
+
coverage_percent = self._read_coverage_percent()
|
|
649
|
+
if coverage_percent is None:
|
|
650
|
+
warning_score = 70 if self.is_zero_to_one else 50
|
|
651
|
+
checks.append(QualityCheck(
|
|
652
|
+
name="测试覆盖率",
|
|
653
|
+
category="testing",
|
|
654
|
+
description="覆盖率报告",
|
|
655
|
+
status=CheckStatus.WARNING,
|
|
656
|
+
score=warning_score,
|
|
657
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
658
|
+
details="未检测到 coverage.xml 报告",
|
|
659
|
+
))
|
|
660
|
+
elif coverage_percent >= 80:
|
|
661
|
+
checks.append(QualityCheck(
|
|
662
|
+
name="测试覆盖率",
|
|
663
|
+
category="testing",
|
|
664
|
+
description="覆盖率报告",
|
|
665
|
+
status=CheckStatus.PASSED,
|
|
666
|
+
score=coverage_percent,
|
|
667
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
668
|
+
details=f"覆盖率 {coverage_percent}%",
|
|
669
|
+
))
|
|
670
|
+
elif coverage_percent >= 60:
|
|
671
|
+
checks.append(QualityCheck(
|
|
672
|
+
name="测试覆盖率",
|
|
673
|
+
category="testing",
|
|
674
|
+
description="覆盖率报告",
|
|
675
|
+
status=CheckStatus.WARNING,
|
|
676
|
+
score=coverage_percent,
|
|
677
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
678
|
+
details=f"覆盖率 {coverage_percent}%(建议提升到 80%+)",
|
|
679
|
+
))
|
|
680
|
+
else:
|
|
681
|
+
checks.append(QualityCheck(
|
|
682
|
+
name="测试覆盖率",
|
|
683
|
+
category="testing",
|
|
684
|
+
description="覆盖率报告",
|
|
685
|
+
status=CheckStatus.FAILED,
|
|
686
|
+
score=coverage_percent,
|
|
687
|
+
weight=self.CHECKS_CONFIG["testing"]["weight"],
|
|
688
|
+
details=f"覆盖率 {coverage_percent}%(低于最低建议)",
|
|
689
|
+
))
|
|
690
|
+
|
|
691
|
+
return checks
|
|
692
|
+
|
|
693
|
+
def _check_code_quality(self) -> list[QualityCheck]:
|
|
694
|
+
"""检查代码质量工具"""
|
|
695
|
+
checks: list[QualityCheck] = []
|
|
696
|
+
|
|
697
|
+
# 检查 Linter
|
|
698
|
+
has_eslint = (self.project_dir / ".eslintrc.js").exists() or (
|
|
699
|
+
self.project_dir / ".eslintrc.json"
|
|
700
|
+
).exists()
|
|
701
|
+
has_pylint = (self.project_dir / "pylint.ini").exists()
|
|
702
|
+
has_black = (self.project_dir / "pyproject.toml").exists() and "black" in (
|
|
703
|
+
self.project_dir / "pyproject.toml"
|
|
704
|
+
).read_text(encoding='utf-8')
|
|
705
|
+
|
|
706
|
+
if has_eslint or has_pylint or has_black:
|
|
707
|
+
checks.append(QualityCheck(
|
|
708
|
+
name="Linter",
|
|
709
|
+
category="code_quality",
|
|
710
|
+
description="代码静态检查工具",
|
|
711
|
+
status=CheckStatus.PASSED,
|
|
712
|
+
score=100,
|
|
713
|
+
weight=self.CHECKS_CONFIG["code_quality"]["weight"],
|
|
714
|
+
details="Linter 已配置",
|
|
715
|
+
))
|
|
716
|
+
else:
|
|
717
|
+
checks.append(QualityCheck(
|
|
718
|
+
name="Linter",
|
|
719
|
+
category="code_quality",
|
|
720
|
+
description="代码静态检查工具",
|
|
721
|
+
status=CheckStatus.WARNING,
|
|
722
|
+
score=50,
|
|
723
|
+
weight=self.CHECKS_CONFIG["code_quality"]["weight"],
|
|
724
|
+
details="Linter 未配置",
|
|
725
|
+
))
|
|
726
|
+
|
|
727
|
+
python_roots = self._discover_python_source_roots()
|
|
728
|
+
if python_roots:
|
|
729
|
+
python_exec = shutil.which("python3") or shutil.which("python")
|
|
730
|
+
if python_exec:
|
|
731
|
+
cmd = [python_exec, "-m", "compileall", "-q", *[str(p) for p in python_roots]]
|
|
732
|
+
result = self._run_command(cmd, timeout=120)
|
|
733
|
+
if result["timed_out"]:
|
|
734
|
+
checks.append(QualityCheck(
|
|
735
|
+
name="Python 语法检查",
|
|
736
|
+
category="code_quality",
|
|
737
|
+
description="compileall 语法检查",
|
|
738
|
+
status=CheckStatus.WARNING,
|
|
739
|
+
score=50,
|
|
740
|
+
weight=self.CHECKS_CONFIG["code_quality"]["weight"],
|
|
741
|
+
details="compileall 执行超时",
|
|
742
|
+
))
|
|
743
|
+
elif result["returncode"] == 0:
|
|
744
|
+
checks.append(QualityCheck(
|
|
745
|
+
name="Python 语法检查",
|
|
746
|
+
category="code_quality",
|
|
747
|
+
description="compileall 语法检查",
|
|
748
|
+
status=CheckStatus.PASSED,
|
|
749
|
+
score=100,
|
|
750
|
+
weight=self.CHECKS_CONFIG["code_quality"]["weight"],
|
|
751
|
+
details="Python 语法检查通过",
|
|
752
|
+
))
|
|
753
|
+
else:
|
|
754
|
+
checks.append(QualityCheck(
|
|
755
|
+
name="Python 语法检查",
|
|
756
|
+
category="code_quality",
|
|
757
|
+
description="compileall 语法检查",
|
|
758
|
+
status=CheckStatus.FAILED,
|
|
759
|
+
score=20,
|
|
760
|
+
weight=self.CHECKS_CONFIG["code_quality"]["weight"],
|
|
761
|
+
details="Python 语法检查失败",
|
|
762
|
+
))
|
|
763
|
+
else:
|
|
764
|
+
checks.append(QualityCheck(
|
|
765
|
+
name="Python 语法检查",
|
|
766
|
+
category="code_quality",
|
|
767
|
+
description="compileall 语法检查",
|
|
768
|
+
status=CheckStatus.WARNING,
|
|
769
|
+
score=50,
|
|
770
|
+
weight=self.CHECKS_CONFIG["code_quality"]["weight"],
|
|
771
|
+
details="未找到 python 解释器,跳过语法检查",
|
|
772
|
+
))
|
|
773
|
+
|
|
774
|
+
return checks
|
|
775
|
+
|
|
776
|
+
def _calculate_total_score(self, checks: list[QualityCheck]) -> int:
|
|
777
|
+
"""计算总分"""
|
|
778
|
+
if not checks:
|
|
779
|
+
return 0
|
|
780
|
+
|
|
781
|
+
return sum(c.score for c in checks) // len(checks)
|
|
782
|
+
|
|
783
|
+
def _calculate_weighted_score(self, checks: list[QualityCheck]) -> float:
|
|
784
|
+
"""计算加权分"""
|
|
785
|
+
if not checks:
|
|
786
|
+
return 0.0
|
|
787
|
+
|
|
788
|
+
total_weight = sum(c.weight for c in checks)
|
|
789
|
+
if total_weight == 0:
|
|
790
|
+
return 0.0
|
|
791
|
+
|
|
792
|
+
weighted_sum = sum(c.score * c.weight for c in checks)
|
|
793
|
+
return weighted_sum / total_weight
|
|
794
|
+
|
|
795
|
+
def _generate_recommendations(self, checks: list[QualityCheck]) -> list[str]:
|
|
796
|
+
"""生成改进建议"""
|
|
797
|
+
recommendations: list[str] = []
|
|
798
|
+
|
|
799
|
+
for check in checks:
|
|
800
|
+
if check.status == CheckStatus.FAILED:
|
|
801
|
+
recommendations.append(f"修复: {check.description}")
|
|
802
|
+
elif check.status == CheckStatus.WARNING:
|
|
803
|
+
recommendations.append(f"建议: {check.description}")
|
|
804
|
+
|
|
805
|
+
return recommendations
|
|
806
|
+
|
|
807
|
+
def _discover_python_tests(self) -> list[Path]:
|
|
808
|
+
roots = [self.project_dir / "tests", self.project_dir / "backend" / "tests"]
|
|
809
|
+
files: list[Path] = []
|
|
810
|
+
for tests_dir in roots:
|
|
811
|
+
if not tests_dir.exists():
|
|
812
|
+
continue
|
|
813
|
+
files.extend(list(tests_dir.rglob("test_*.py")))
|
|
814
|
+
files.extend(list(tests_dir.rglob("*_test.py")))
|
|
815
|
+
return files
|
|
816
|
+
|
|
817
|
+
def _has_pytest_config(self) -> bool:
|
|
818
|
+
if (self.project_dir / "pytest.ini").exists():
|
|
819
|
+
return True
|
|
820
|
+
|
|
821
|
+
setup_cfg = self.project_dir / "setup.cfg"
|
|
822
|
+
if setup_cfg.exists():
|
|
823
|
+
content = setup_cfg.read_text(encoding="utf-8", errors="ignore")
|
|
824
|
+
if "[tool:pytest]" in content:
|
|
825
|
+
return True
|
|
826
|
+
|
|
827
|
+
tox_ini = self.project_dir / "tox.ini"
|
|
828
|
+
if tox_ini.exists():
|
|
829
|
+
content = tox_ini.read_text(encoding="utf-8", errors="ignore")
|
|
830
|
+
if "[pytest]" in content:
|
|
831
|
+
return True
|
|
832
|
+
|
|
833
|
+
pyproject = self.project_dir / "pyproject.toml"
|
|
834
|
+
if pyproject.exists():
|
|
835
|
+
content = pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
836
|
+
if "pytest.ini_options" in content:
|
|
837
|
+
return True
|
|
838
|
+
|
|
839
|
+
backend_pyproject = self.project_dir / "backend" / "pyproject.toml"
|
|
840
|
+
if backend_pyproject.exists():
|
|
841
|
+
content = backend_pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
842
|
+
if "pytest.ini_options" in content:
|
|
843
|
+
return True
|
|
844
|
+
|
|
845
|
+
return False
|
|
846
|
+
|
|
847
|
+
def _discover_python_source_roots(self) -> list[Path]:
|
|
848
|
+
roots: list[Path] = []
|
|
849
|
+
candidates = [
|
|
850
|
+
"super_dev", "src", "app", "backend", "server", "api", "services", "lib"
|
|
851
|
+
]
|
|
852
|
+
for name in candidates:
|
|
853
|
+
path = self.project_dir / name
|
|
854
|
+
if path.exists() and path.is_dir():
|
|
855
|
+
roots.append(path)
|
|
856
|
+
|
|
857
|
+
top_level_py = list(self.project_dir.glob("*.py"))
|
|
858
|
+
roots.extend(top_level_py)
|
|
859
|
+
|
|
860
|
+
# 去重并限制数量,避免无界扫描
|
|
861
|
+
unique: list[Path] = []
|
|
862
|
+
seen = set()
|
|
863
|
+
for path in roots:
|
|
864
|
+
key = str(path.resolve())
|
|
865
|
+
if key in seen:
|
|
866
|
+
continue
|
|
867
|
+
seen.add(key)
|
|
868
|
+
unique.append(path)
|
|
869
|
+
return unique[:8]
|
|
870
|
+
|
|
871
|
+
def _discover_js_test_targets(self) -> list[Path]:
|
|
872
|
+
targets: list[Path] = []
|
|
873
|
+
for root in (self.project_dir, self.project_dir / "frontend", self.project_dir / "backend"):
|
|
874
|
+
package_json = root / "package.json"
|
|
875
|
+
if not package_json.exists():
|
|
876
|
+
continue
|
|
877
|
+
try:
|
|
878
|
+
data = json.loads(package_json.read_text(encoding="utf-8"))
|
|
879
|
+
except Exception:
|
|
880
|
+
continue
|
|
881
|
+
scripts = data.get("scripts", {})
|
|
882
|
+
test_script = str(scripts.get("test", "")).strip()
|
|
883
|
+
if not test_script:
|
|
884
|
+
continue
|
|
885
|
+
if test_script == 'echo "Error: no test specified" && exit 1':
|
|
886
|
+
continue
|
|
887
|
+
targets.append(root)
|
|
888
|
+
return targets
|
|
889
|
+
|
|
890
|
+
def _has_js_test_script(self) -> bool:
|
|
891
|
+
return bool(self._discover_js_test_targets())
|
|
892
|
+
|
|
893
|
+
def _extract_test_summary(self, output: str) -> str:
|
|
894
|
+
if not output:
|
|
895
|
+
return ""
|
|
896
|
+
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
897
|
+
for line in reversed(lines):
|
|
898
|
+
if "passed" in line or "failed" in line or "error" in line:
|
|
899
|
+
return line[:240]
|
|
900
|
+
return lines[-1][:240] if lines else ""
|
|
901
|
+
|
|
902
|
+
def _read_coverage_percent(self) -> int | None:
|
|
903
|
+
coverage_candidates = [
|
|
904
|
+
self.project_dir / "coverage.xml",
|
|
905
|
+
self.project_dir / "backend" / "coverage.xml",
|
|
906
|
+
self.project_dir / "frontend" / "coverage.xml",
|
|
907
|
+
self.project_dir / "coverage" / "cobertura-coverage.xml",
|
|
908
|
+
self.project_dir / "frontend" / "coverage" / "cobertura-coverage.xml",
|
|
909
|
+
self.project_dir / "backend" / "coverage" / "cobertura-coverage.xml",
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
parsed_values: list[int] = []
|
|
913
|
+
for coverage_xml in coverage_candidates:
|
|
914
|
+
if not coverage_xml.exists():
|
|
915
|
+
continue
|
|
916
|
+
try:
|
|
917
|
+
root = ElementTree.fromstring(coverage_xml.read_text(encoding="utf-8"))
|
|
918
|
+
line_rate = root.attrib.get("line-rate")
|
|
919
|
+
if line_rate is not None:
|
|
920
|
+
parsed_values.append(max(0, min(100, int(round(float(line_rate) * 100)))))
|
|
921
|
+
continue
|
|
922
|
+
|
|
923
|
+
lines_covered = root.attrib.get("lines-covered")
|
|
924
|
+
lines_valid = root.attrib.get("lines-valid")
|
|
925
|
+
if lines_covered and lines_valid and float(lines_valid) > 0:
|
|
926
|
+
percent = (float(lines_covered) / float(lines_valid)) * 100
|
|
927
|
+
parsed_values.append(max(0, min(100, int(round(percent)))))
|
|
928
|
+
except Exception:
|
|
929
|
+
continue
|
|
930
|
+
|
|
931
|
+
if not parsed_values:
|
|
932
|
+
return None
|
|
933
|
+
return max(parsed_values)
|
|
934
|
+
|
|
935
|
+
def _run_command(self, cmd: list[str], timeout: int = 120) -> dict[str, object]:
|
|
936
|
+
try:
|
|
937
|
+
completed = subprocess.run(
|
|
938
|
+
cmd,
|
|
939
|
+
cwd=str(self.project_dir),
|
|
940
|
+
capture_output=True,
|
|
941
|
+
text=True,
|
|
942
|
+
timeout=timeout,
|
|
943
|
+
check=False,
|
|
944
|
+
) # nosec B603
|
|
945
|
+
return {
|
|
946
|
+
"returncode": completed.returncode,
|
|
947
|
+
"stdout": completed.stdout or "",
|
|
948
|
+
"stderr": completed.stderr or "",
|
|
949
|
+
"timed_out": False,
|
|
950
|
+
}
|
|
951
|
+
except subprocess.TimeoutExpired as e:
|
|
952
|
+
return {
|
|
953
|
+
"returncode": -1,
|
|
954
|
+
"stdout": (e.stdout or ""),
|
|
955
|
+
"stderr": (e.stderr or ""),
|
|
956
|
+
"timed_out": True,
|
|
957
|
+
}
|
|
958
|
+
except Exception as e:
|
|
959
|
+
return {
|
|
960
|
+
"returncode": -1,
|
|
961
|
+
"stdout": "",
|
|
962
|
+
"stderr": str(e),
|
|
963
|
+
"timed_out": False,
|
|
964
|
+
}
|