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"| Field | Status | COBOL | " \
- f"Java | Suggestion |
{rows}
"
+
+ # 覆盖率卡片
+ 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}
+字段比对
+
+| Field | Status | COBOL | Java | Suggestion |
+{rows}
+{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: