feat: Phase 3+4 - gcov support + enhanced report

This commit is contained in:
hangshuo652
2026-06-18 16:31:54 +08:00
parent e2486db510
commit c93104e6bf
3 changed files with 131 additions and 10 deletions
+57
View File
@@ -0,0 +1,57 @@
import subprocess
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
def collect_gcov(cobol_src: Path, work_dir: Path) -> dict:
try:
gcda_files = list(work_dir.glob("*.gcda"))
if not gcda_files:
logger.warning("[gcov] 未找到 .gcda 文件,可能未启用插桩编译")
return {"available": False, "reason": "no_gcda_files"}
result = subprocess.run(
["gcov", cobol_src.name],
capture_output=True, text=True, timeout=30,
cwd=work_dir,
)
if result.returncode != 0:
logger.warning(f"[gcov] gcov 执行失败: {result.stderr[:200]}")
return {"available": False, "reason": "gcov_failed"}
gcov_file = work_dir / f"{cobol_src.stem}.cbl.gcov"
if not gcov_file.exists():
gcov_file = work_dir / f"{cobol_src.stem}.gcov"
if not gcov_file.exists():
logger.warning("[gcov] .gcov 文件未生成")
return {"available": False, "reason": "no_gcov_output"}
total_lines = 0
executed_lines = 0
with open(gcov_file) as f:
for line in f:
stripped = line.strip()
if stripped and not stripped.startswith("-"):
total_lines += 1
if not stripped.startswith("#"):
executed_lines += 1
line_rate = executed_lines / max(total_lines, 1)
return {
"available": True,
"line_rate": round(line_rate, 4),
"total_lines": total_lines,
"executed_lines": executed_lines,
}
except FileNotFoundError:
logger.warning("[gcov] gcov 命令未找到,降级为仅静态分析")
return {"available": False, "reason": "gcov_not_installed"}
except Exception as e:
logger.warning(f"[gcov] 采集异常: {e}")
return {"available": False, "reason": str(e)[:100]}
+69 -7
View File
@@ -21,13 +21,75 @@ class ReportGenerator:
f'</td><td>{fr.status}</td><td>{fr.cobol_value}</td><td>{fr.java_value}</td>'
f'<td>{fr.suggestion}</td></tr>'
for fr in run.field_results)
html = f"<!DOCTYPE html><html><head><meta charset=utf-8><title>{run.program}</title>" \
f"<style>body{{font-family:monospace;max-width:900px;margin:2rem auto}}" \
f".pass{{background:#e6ffe6}}.fail{{background:#ffe6e6}}pre{{background:#f0f0f0;padding:1rem}}" \
f"</style></head><body><h1>{run.program}</h1><pre>Status: {run.status} | " \
f"Runner: {run.runner} | {run.fields_matched} fields | {run.duration_s}s</pre>" \
f"<table border=1 cellpadding=4><tr><th>Field</th><th>Status</th><th>COBOL</th>" \
f"<th>Java</th><th>Suggestion</th></tr>{rows}</table></body></html>"
# 覆盖率卡片
coverage_html = ""
if run.paragraph_rate > 0 or run.branch_rate > 0:
mode = "静态+动态" if run.branch_rate > 0 else "仅静态"
pcolor = "green" if run.paragraph_rate >= 1.0 else "orange"
bcolor = "green" if run.branch_rate >= 0.9 else "orange"
coverage_html = f"""
<h2>覆盖率</h2>
<table border=1 cellpadding=4>
<tr><td>方式</td><td>{mode}</td></tr>
<tr><td>段落覆盖率</td><td style="color:{pcolor}">{run.paragraph_rate:.0%}</td></tr>
<tr><td>分支覆盖率</td><td style="color:{bcolor}">{run.branch_rate:.0%}</td></tr>
<tr><td>决策点覆盖率</td><td>{run.decision_rate:.0%}</td></tr>
</table>"""
# HINA 卡片
hina_html = ""
if run.hina_type:
hina_html = f"""
<h2>HINA 信息</h2>
<table border=1 cellpadding=4>
<tr><td>判定类型</td><td>{run.hina_type}</td></tr>
<tr><td>确信度</td><td>{run.hina_confidence:.0%}</td></tr>
</table>"""
# 质量评分卡片
quality_html = ""
if run.quality_score > 0:
color = "green" if run.quality_score >= 0.8 else "orange"
quality_html = f"""
<h2>质量评分</h2>
<div style="font-size:2rem;color:{color};font-weight:bold">{run.quality_score:.0%}</div>"""
# 重试历史卡片
retry_html = ""
if run.total_retry > 0:
retry_html = f"""
<h2>重试历史</h2>
<table border=1 cellpadding=4>
<tr><td>heal_retry</td><td>{run.heal_retry}</td></tr>
<tr><td>simple_retry</td><td>{run.simple_retry}</td></tr>
<tr><td>total_retry</td><td>{run.total_retry}</td></tr>
</table>"""
warn_html = ""
if run.quality_warn:
warn_html = f'<div style="background:#fff3cd;padding:1rem;margin:1rem 0">{run.quality_warn}</div>'
html = f"""<!DOCTYPE html>
<html><head><meta charset=utf-8><title>{run.program}</title>
<style>
body{{font-family:monospace;max-width:900px;margin:2rem auto}}
.pass{{background:#e6ffe6}}.fail{{background:#ffe6e6}}
pre{{background:#f0f0f0;padding:1rem}}
table{{border-collapse:collapse}} td,th{{padding:6px 12px}}
</style></head><body>
<h1>{run.program}</h1>
<pre>Status: {run.status} | Runner: {run.runner} | {run.fields_matched} matched | {run.duration_s:.0f}s</pre>
{warn_html}
<h2>字段比对</h2>
<table border=1 cellpadding=4>
<tr><th>Field</th><th>Status</th><th>COBOL</th><th>Java</th><th>Suggestion</th></tr>
{rows}</table>
{coverage_html}
{hina_html}
{quality_html}
{retry_html}
</body></html>"""
p.write_text(html)
return p
+5 -3
View File
@@ -4,11 +4,13 @@ from runners.runner import BuildResult, RunResult
class CobolRunner:
def compile(self, src: str, dialect="ibm") -> BuildResult:
def compile(self, src: str, dialect="ibm", gcov: bool = False) -> BuildResult:
stem = Path(src).stem
out = str(Path(src).parent / stem)
p = subprocess.run(["cobc", "-x", f"-std={dialect}-strict", "-o", out, src],
capture_output=True, text=True, timeout=30)
cmd = ["cobc", "-x", f"-std={dialect}-strict", "-o", out, src]
if gcov:
cmd = ["cobc", "-x", f"-std={dialect}-strict", "-fprofile-arcs", "-ftest-coverage", "-o", out, src]
p = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return BuildResult(success=p.returncode == 0, artifact_path=out, log=p.stdout + p.stderr)
def run(self, binary: str, input_path: str, output_path: str) -> RunResult: