"""gcov 覆盖率数据解析和分支标记""" import re import logging import subprocess from pathlib import Path logger = logging.getLogger(__name__) def parse_cbl_gcov(gcov_path: str) -> dict[int, int]: """解析 .cbl.gcov 文件,返回 {COBOL行号: 执行次数}。 gcov 行格式: #####: 6: 源码行 → 未执行(0 次) 75*: 12: 源码行 → 执行 75 次 1*: 14: 源码行 → 执行 1 次 -: 17: 源码行 → 不可执行(注释/声明行,跳过) """ counts = {} with open(gcov_path, encoding='utf-8', errors='replace') as f: for line in f: m = re.match(r'^\s*(#####|\d+\*?|-):\s*(\d+):', line) if not m: continue count_str = m.group(1) lineno = int(m.group(2)) if count_str == '#####': counts[lineno] = 0 elif count_str == '-': continue else: counts[lineno] = int(count_str.rstrip('*')) return counts def run_gcov(program_name: str, work_dir: str) -> dict[int, int]: """在 work_dir 中执行 gcov 并解析 COBOL 行计数。 使用 Windows 本地的 gcov(与 GnuCOBOL 内置 MinGW 同版本)。 不要通过 WSL 调用 gcov,否则 .gcno/.gcda 版本不匹配。 Args: program_name: 程序名(不含扩展名),如 "ALLCMDS" work_dir: 包含 .gcda/.gcno 的目录(Windows 路径) Returns: {COBOL行号: 执行次数} 字典。失败时返回空 dict。 """ try: result = subprocess.run( ['gcov', f'{program_name}.cbl'], capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=30, cwd=work_dir, ) except FileNotFoundError: logger.warning("gcov 命令未找到(未安装 MinGW gcov 或不在 PATH 中)") return {} if result.returncode != 0: logger.warning(f"gcov 失败 (exit={result.returncode}): {result.stderr.strip()}") return {} cbl_gcov = Path(work_dir) / f'{program_name}.cbl.gcov' if not cbl_gcov.exists(): cbl_gcov = Path(work_dir) / f'{program_name}.gcov' if not cbl_gcov.exists(): logger.warning(f"gcov 输出不存在 (tried .cbl.gcov / .gcov)") return {} gcov_data = parse_cbl_gcov(str(cbl_gcov)) logger.info(f"gcov 解析: {len(gcov_data)} 行, " f"{sum(1 for v in gcov_data.values() if v > 0)} 行已执行") return gcov_data def _wsl_path(windows_path: str) -> str: path = Path(windows_path).resolve() drive = path.drive.lower().rstrip(':') rest = str(path.relative_to(path.anchor)).replace('\\', '/') return f'/mnt/{drive}/{rest}' def _find_if_body_lines(source_lines: list[str], if_lineno_1: int): """在源码中定位 IF 语句的 THEN/ELSE 体行范围(行号 1-indexed)。 Returns (then_lines, else_lines): then_lines: list[int] — THEN 体的行号(1-indexed) else_lines: list[int] — ELSE 体的行号(1-indexed),无 ELSE 则为空 """ start = if_lineno_1 # 0-indexed, start AFTER the IF line depth = 1 else_start_0 = None end_if_0 = None n = len(source_lines) for i in range(start, n): line = source_lines[i].upper().strip() if re.match(r'ELSE\s+IF', line, re.IGNORECASE): if depth == 1: else_start_0 = i depth += 1 continue if re.match(r'\bIF\b', line): depth += 1 if re.match(r'END-IF', line): depth -= 1 if '.' in line: depth = 0 if depth <= 0: end_if_0 = i break if depth == 1 and re.match(r'ELSE\b', line): else_start_0 = i then_start_1 = if_lineno_1 + 1 if else_start_0 is not None: then_1 = list(range(then_start_1, else_start_0 + 1)) else_1 = list(range(else_start_0 + 2, (end_if_0 or start) + 2)) else: then_1 = list(range(then_start_1, (end_if_0 or start) + 2)) else_1 = [] return then_1, else_1 def mark_from_gcov(decision_points: list, gcov_data: dict[int, int], branch_tree, source_text: str | None = None) -> None: """用 gcov 行执行计数推断决策点分支覆盖,直接修改 decision_points 的 active_branches。 当 source_text 提供时(预处理源码),IF 分支使用体行计数精确判断 T/F。 IF (条件行 L): - 体行计数 > 0 → 对应分支覆盖(T=THEN体,F=ELSE体) - 无体行数据时回退:count==0 跳过,count>0 标记 T/F EVALUATE: - subject 行 count > 0 → 标记所有 WHEN 为已覆盖 PERFORM UNTIL (条件行 L): - count == 1 → 条件初始即为真,循环体未进入 → Skip 覆盖 - count > 1 → 循环体至少进入一次 → Enter 覆盖 - Skip 总视为覆盖(无论进入与否,最终都会跳出) """ source_lines = source_text.splitlines() if source_text else None for dp in decision_points: ln = dp.source_line if ln <= 0 or ln not in gcov_data: continue count = gcov_data.get(ln) if count is None: continue if dp.kind == 'IF': # 清除静态分析的 IF 标记,用 gcov 运行时数据重新判断 dp.active_branches.discard('T') dp.active_branches.discard('F') if source_lines and ln <= len(source_lines): then_lines, else_lines = _find_if_body_lines(source_lines, ln) then_cov = any(gcov_data.get(tl, 0) > 0 for tl in then_lines) else_cov = any(gcov_data.get(el, 0) > 0 for el in else_lines) if then_cov: dp.active_branches.add('T') if else_cov: dp.active_branches.add('F') # P0: ELSE-less IF — F is structurally mandatory when IF line executed if not else_lines and then_lines and count > 0: dp.active_branches.add('F') # 如果体行范围为空或无法判断,回退到基于 IF 行计数 if not then_lines and not else_lines: if count > 0: dp.active_branches.add('T') dp.active_branches.add('F') else: # 无源码文本回退到原逻辑 if count > 0: dp.active_branches.add('T') dp.active_branches.add('F') elif dp.kind == 'EVALUATE': for bn in dp.branch_names: dp.active_branches.discard(bn) if count == 0: continue for bn in dp.branch_names: dp.active_branches.add(bn) elif dp.kind == 'PERFORM': dp.active_branches.discard('Enter') dp.active_branches.discard('Skip') if count > 1: dp.active_branches.add('Enter') dp.active_branches.add('Skip')