feat: Phase 3+4 - gcov support + enhanced report
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user