181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
"""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'\bIF\b', line) and not re.match(r'ELSE\s+IF', line, re.IGNORECASE):
|
||
depth += 1
|
||
if re.match(r'END-IF', line):
|
||
depth -= 1
|
||
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')
|
||
# 如果体行范围为空或无法判断,回退到基于 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':
|
||
if count == 0:
|
||
continue
|
||
for bn in dp.branch_names:
|
||
dp.active_branches.add(bn)
|
||
|
||
elif dp.kind == 'PERFORM':
|
||
if count > 1:
|
||
dp.active_branches.add('Enter')
|
||
dp.active_branches.add('Skip')
|