From c93104e6bf1d491573cb17905251dbb7b72e480c Mon Sep 17 00:00:00 2001 From: hangshuo652 Date: Thu, 18 Jun 2026 16:31:54 +0800 Subject: [PATCH] feat: Phase 3+4 - gcov support + enhanced report --- hina/gcov_collector.py | 57 +++++++++++++++++++++++++++++++ report/generator.py | 76 +++++++++++++++++++++++++++++++++++++---- runners/cobol_runner.py | 8 +++-- 3 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 hina/gcov_collector.py diff --git a/hina/gcov_collector.py b/hina/gcov_collector.py new file mode 100644 index 0000000..51ccc26 --- /dev/null +++ b/hina/gcov_collector.py @@ -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]} diff --git a/report/generator.py b/report/generator.py index afba276..caee157 100644 --- a/report/generator.py +++ b/report/generator.py @@ -21,13 +21,75 @@ class ReportGenerator: f'{fr.status}{fr.cobol_value}{fr.java_value}' f'{fr.suggestion}' for fr in run.field_results) - html = f"{run.program}" \ - f"

{run.program}

Status: {run.status} | " \
-               f"Runner: {run.runner} | {run.fields_matched} fields | {run.duration_s}s
" \ - f"" \ - f"{rows}
FieldStatusCOBOLJavaSuggestion
" + + # 覆盖率卡片 + 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""" +

覆盖率

+ + + + + +
方式{mode}
段落覆盖率{run.paragraph_rate:.0%}
分支覆盖率{run.branch_rate:.0%}
决策点覆盖率{run.decision_rate:.0%}
""" + + # HINA 卡片 + hina_html = "" + if run.hina_type: + hina_html = f""" +

HINA 信息

+ + + +
判定类型{run.hina_type}
确信度{run.hina_confidence:.0%}
""" + + # 质量评分卡片 + quality_html = "" + if run.quality_score > 0: + color = "green" if run.quality_score >= 0.8 else "orange" + quality_html = f""" +

质量评分

+
{run.quality_score:.0%}
""" + + # 重试历史卡片 + retry_html = "" + if run.total_retry > 0: + retry_html = f""" +

重试历史

+ + + + +
heal_retry{run.heal_retry}
simple_retry{run.simple_retry}
total_retry{run.total_retry}
""" + + warn_html = "" + if run.quality_warn: + warn_html = f'
{run.quality_warn}
' + + html = f""" +{run.program} + +

{run.program}

+
Status: {run.status} | Runner: {run.runner} | {run.fields_matched} matched | {run.duration_s:.0f}s
+{warn_html} +

字段比对

+ + +{rows}
FieldStatusCOBOLJavaSuggestion
+{coverage_html} +{hina_html} +{quality_html} +{retry_html} +""" p.write_text(html) return p diff --git a/runners/cobol_runner.py b/runners/cobol_runner.py index a106e54..7718816 100644 --- a/runners/cobol_runner.py +++ b/runners/cobol_runner.py @@ -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: