Files
hangshuo652 54d4e81240 feat: phase2 review fixes
- TIME injection: change from spaces to '2500' (hour>23) for NUMVAL trigger
- Runner: add .resolve() to work_dir to fix chdir+relative path breakage
- Coverage: per-target field overrides for DP#9-#12 (START-DATE=20240115 etc.)
- .gitignore: add compilation artifacts, temp scripts, test outputs
2026-07-15 21:46:28 +08:00

195 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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')