Files
cobol-java-v3/cobol_testgen/design_mcdc.py
T
NB-076 e5ab3baa46 提升:37/37基准程序全量解析+O(N)路径枚举+运行时gcov验证
## 核心变更

### 1. 新PROCEDURE DIVISION解析器(procedure_parser.py)
- 行级状态机替换旧的BrParser regex解析器
- 覆盖:IF/ELSE/END-IF(嵌套)、EVALUATE/WHEN/ALSO、
  PERFORM UNTIL/VARYING、READ/AT END/NOT AT END、
  SORT/MERGE、GO TO DEPENDING ON
- 之前:3/37程序有分支检测  →  现在:37/37全部有分支
- 速度:~20ms/程序,纯规则引擎

### 2. 桥接层(pipeline_bridge.py)
- 新解析器为主,旧解析器3秒超时兜底
- 自动选取分支数更多的结果

### 3. 线性路径枚举(design_mcdc.py)
- 替换旧的Cartesian积路径枚举(O(2^N))为每决策点独立枚举(O(N))
- 28-sysin: 162分支仅163条路径(之前需截断到60DP)
- 消除了500路径硬上限和60DP截断

### 4. 条件解析修复(cond.py)
- NOT运算符规范化:X NOT = 5 → X <> 5
- 88-level反向:NOT WS-EOF-Y → parent <> value
- 裸字段引用:NOT WS-EOF → WS-EOF <> 'Y'
- 验证:1182个IF条件中0个NOT污染

### 5. 约束字段过滤(__init__.py)
- OF限定词剥离:STD-KEY OF MASTER-REC → STD-KEY
- 下标字段解析:WS-ITEM(SUB) → WS-ITEM
- 跳过不在fields_dict中的字段(group item/伪影)

### 6. 预处理器增强(read.py)
- VALUE ALL剥离(VALUE ALL '*' → VALUE '*')
- &续行合并(COBOL多行字符串拼接)
- PIC小数点点→V转换(Z(9)9.99. → Z(9)9V99.)
- 缺少点号补全

### 7. Grammar修复(grammar.lark)
- OCCURS 1 TIME支持(原只认TIMES)
- USAGE IS COMP支持(可选IS)
- $符号在PICTURE_STRING中
- 无NAME条款支持(clause+)

### 8. Flatfile写入(flatfile.py)
- 多记录FD支持(选字段最多的记录)
- Path类型强制转换
- 回退零值记录

### 9. Bug修复
- trace_to_root空列表保护(core.py)

### 10. 测试套件(S16-S21)
- S16: 全量基准程序端到端
- S17: gcov运行时对比
- S18/S19: 桥接器验证
- S20: DISPLAY插桩运行时验证+gcov分支覆盖率
- S21: 条件解析修复验证
- 全部17/17回归测试通过

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 23:41:22 +08:00

234 lines
8.3 KiB
Python

"""Non-exploding path enumeration — per-decision-point coverage, O(N) paths.
Strategy:
1. Walk the tree once to collect ALL decision points and their "access paths"
2. For each decision point D, generate 2 paths:
- D=True with ancestor and descendant access constraints
- D=False with ancestor and descendant access constraints
3. Total: 2 * N paths, where N = number of decision points
This guarantees every branch is exercised at least once, without O(2^N) explosion.
"""
import re
import logging
from .models import BrSeq, BrIf, BrEval, BrPerform, BrSearch, Assign, CallNode, CondNot, CondLeaf, ExitNode, GoTo
from .cond import parse_single_condition, parse_compound_condition, is_field, collect_leaves, mcdc_sets
logger = logging.getLogger(__name__)
_STOP = ('__STOP__', '', None, True)
def _parse_condition(condition_text, fields):
"""Parse an IF condition into (field, op, value) or None."""
parsed = parse_single_condition(condition_text, fields)
if parsed and is_field(parsed[0], fields):
return parsed
if parsed:
return parsed
return None
def _invert_condition(parsed):
"""Invert a parsed condition (True ↔ False)."""
if parsed is None:
return None
field, op, val = parsed
inv_op = {'=': '<>', '<>': '=', '>': '<=', '<': '>=', '>=': '<', '<=': '>'}.get(op, op)
return (field, inv_op, val)
# ── Collect all decision points with access paths ──
def _collect_all_dps(node, fields, path_cons=None, path_assign=None, depth=0):
"""Walk tree, collect list of (decision_point, access_path) tuples.
Returns list of dicts:
{ "node": decision_point_node,
"kind": "IF"|"EVALUATE"|"PERFORM"|"SEARCH"|"AT_END",
"access_constraints": [constraints to reach this point],
"branches": list of (branch_label, body_node_children)
"true_idx": index of "True" branch in branches,
"false_idx": index of "False" branch (or None),
}
"""
path_cons = list(path_cons or [])
path_assign = dict(path_assign or {})
result = []
if isinstance(node, BrIf):
parsed = _parse_condition(node.condition, fields)
dp = {
"node": node, "kind": "IF",
"condition": node.condition,
"parsed": parsed,
"access_constraints": list(path_cons),
"true_idx": 0,
"false_idx": 1 if parsed else None,
}
result.append(dp)
# Recurse into both branches
t_cons = list(path_cons)
f_cons = list(path_cons)
if parsed:
field, op, val = parsed
t_cons.append((field, op, val, True))
f_cons.append((field, op, val, False))
result.extend(_collect_all_dps(node.true_seq, fields, t_cons, path_assign, depth + 1))
result.extend(_collect_all_dps(node.false_seq, fields, f_cons, path_assign, depth + 1))
elif isinstance(node, BrEval):
dp = {
"node": node, "kind": "EVALUATE",
"subject": node.subject,
"access_constraints": list(path_cons),
}
result.append(dp)
for value, seq in node.when_list:
w_cons = list(path_cons)
if is_field(node.subject, fields):
w_cons.append((node.subject, '=', value, True))
result.extend(_collect_all_dps(seq, fields, w_cons, path_assign, depth + 1))
if node.has_other:
result.extend(_collect_all_dps(node.other_seq, fields, list(path_cons), path_assign, depth + 1))
elif isinstance(node, BrPerform):
if node.perf_type in ('until', 'para_until', 'varying', 'para_varying'):
parsed = _parse_condition(node.condition, fields)
dp = {
"node": node, "kind": "PERFORM",
"condition": node.condition,
"parsed": parsed,
"access_constraints": list(path_cons),
}
result.append(dp)
if parsed:
field, op, val = parsed
body_cons = list(path_cons) + [(field, op, val, False)]
else:
body_cons = list(path_cons)
result.extend(_collect_all_dps(node.body_seq, fields, body_cons, path_assign, depth + 1))
else:
result.extend(_collect_all_dps(node.body_seq, fields, list(path_cons), path_assign, depth + 1))
elif isinstance(node, BrSeq):
for child in node.children:
result.extend(_collect_all_dps(child, fields, path_cons, path_assign, depth))
elif isinstance(node, BrSearch):
dp = {
"node": node, "kind": "SEARCH",
"access_constraints": list(path_cons),
}
result.append(dp)
result.extend(_collect_all_dps(node.at_end_seq, fields, list(path_cons), path_assign, depth + 1))
for _, seq in node.when_list:
result.extend(_collect_all_dps(seq, fields, list(path_cons), path_assign, depth + 1))
return result
def _make_path_for_branch(dp, branch_idx, fields):
"""Create a single path (constraints, assignments) for one branch of a decision point."""
constraints = list(dp.get("access_constraints", []))
kind = dp["kind"]
if kind == "IF":
parsed = dp.get("parsed")
if parsed is None:
return ([], {})
field, op, val = parsed
want_true = (branch_idx == dp.get("true_idx", 0))
if not want_true:
field2, op2, val2 = _invert_condition(parsed)
field, op, val = field2, op2, val2
constraints.append((field, op, val, True))
# Pick body, just take first assignment
node = dp["node"]
body_seq = node.true_seq if branch_idx == 0 else node.false_seq
return (constraints, {})
if kind == "EVALUATE":
node = dp["node"]
n_when = len(node.when_list)
if branch_idx < n_when:
value, seq = node.when_list[branch_idx]
if is_field(node.subject, []):
constraints.append((node.subject, '=', value, True))
prior_cases = [v for v, _ in node.when_list[:branch_idx]]
for prior in prior_cases:
constraints.append((node.subject, '<>', prior, True))
return (constraints, {})
if kind == "PERFORM":
parsed = dp.get("parsed")
if parsed is None:
return ([], {})
field, op, val = parsed
if branch_idx == 0:
constraints.append((field, op, val, False))
else:
constraints.append((field, op, val, True))
return (constraints, {})
return ([], {})
# ── Public API ──
def enum_paths(node, fields):
"""Linear path enumeration: one True + one False per decision point.
Returns list of (constraints, assignments) tuples.
Total paths = 2 * number_of_decision_points (capped at 1000).
"""
all_dps = _collect_all_dps(node, fields)
MAX_PATH = 1000
paths = []
# Start with one neutral path (no constraints)
paths.append(([], {}))
for dp in all_dps:
kind = dp["kind"]
if kind == "IF":
true_path = _make_path_for_branch(dp, dp.get("true_idx", 0), fields)
false_path = _make_path_for_branch(dp, dp.get("false_idx", 1) if dp.get("false_idx") is not None else dp.get("true_idx", 0), fields)
if true_path:
paths.append(true_path)
if false_path:
paths.append(false_path)
elif kind == "EVALUATE":
node = dp["node"]
for i in range(len(node.when_list)):
bp = _make_path_for_branch(dp, i, fields)
if bp: paths.append(bp)
if node.has_other:
other_cons = list(dp.get("access_constraints", []))
for v, _ in node.when_list:
if is_field(node.subject, []):
other_cons.append((node.subject, '<>', v, True))
paths.append((other_cons, {}))
elif kind == "PERFORM":
enter_path = _make_path_for_branch(dp, 0, fields)
skip_path = _make_path_for_branch(dp, 1, fields)
if enter_path: paths.append(enter_path)
if skip_path: paths.append(skip_path)
if len(paths) >= MAX_PATH:
paths = paths[:MAX_PATH]
break
return paths
def _filter_stop(cons):
return [c for c in cons if c is not _STOP]