fix: 覆盖率统计全面修复 + 5漏洞修正
## 修复内容 ### C1: _mark_eval 反向操作符 (coverage.py) - EVALUATE 约束匹配支持 操作符 - WHEN OTHER 的自动检测(全部 WHEN 被否定时) ### C2: _mark_perform 反向操作符 (coverage.py) - PERFORM 同 _mark_if 的反向操作符匹配 - PERFORM UNTIL 条件截断后桥接器通过 branch_names 识别类型 ### H1: parse_single_condition 传递 fields (coverage.py) - collect_decision_points 调用时传 fields 参数 - NOT 前缀条件解析 (NOT WS-X > 50 → WS-X <= 50) ### H4: generate_data 输入约束 (__init__.py) - 文档注明接收原始源码,非预处理后文本 ### M1: not_map break (cond.py) - NOT 操作符映射循环添加 break ## 覆盖测试结果 - IF: 100% (T/F) - NOT IF: 100% (NOT_TRUE/NOT_FALSE) - PERFORM UNTIL: 100% (ENTER/SKIP) - EVALUATE: 100% (4 WHENs) - Nested IF: 100% (4 branches) - S15 回归: 17/17 PASS Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ from .pipeline_bridge import build_branch_tree_fallback
|
|||||||
from .design_mcdc import enum_paths as mcdc_enum_paths, _filter_stop
|
from .design_mcdc import enum_paths as mcdc_enum_paths, _filter_stop
|
||||||
from .design import enum_paths, generate_records, get_term_type, extend_abend_programs
|
from .design import enum_paths, generate_records, get_term_type, extend_abend_programs
|
||||||
from .output import output_json, output_input_files
|
from .output import output_json, output_input_files
|
||||||
from .coverage import run_coverage, generate_coverage_index
|
from .coverage import run_coverage, generate_coverage_index, collect_decision_points, mark_coverage
|
||||||
from japanese_data import generate_fullwidth_text, generate_halfwidth_katakana, generate_wareki_date
|
from japanese_data import generate_fullwidth_text, generate_halfwidth_katakana, generate_wareki_date
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -935,7 +935,9 @@ def generate_data(cobol_source: str, structure: dict = None) -> list[dict]:
|
|||||||
"""根据 COBOL 源码生成覆盖所有路径的测试数据。
|
"""根据 COBOL 源码生成覆盖所有路径的测试数据。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cobol_source: COBOL 程序源码文本
|
cobol_source: COBOL 程序原始源码文本(未预处理)。
|
||||||
|
内部会调 preprocess + resolve_copybooks。
|
||||||
|
如果已预处理过,传进来会因 COPYBOOK 路径丢失导致字段不全。
|
||||||
structure: 可选,如果已调用 extract_structure() 可传入避免重复解析
|
structure: 可选,如果已调用 extract_structure() 可传入避免重复解析
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -1010,6 +1012,28 @@ def generate_data(cobol_source: str, structure: dict = None) -> list[dict]:
|
|||||||
|
|
||||||
records, kept_paths, term_types = generate_records(path_infos, fields_dict, assignments, file_sec=file_sec)
|
records, kept_paths, term_types = generate_records(path_infos, fields_dict, assignments, file_sec=file_sec)
|
||||||
|
|
||||||
|
# ── Coverage marking: which decision branches are actually covered ──
|
||||||
|
if branch_tree and fields_dict:
|
||||||
|
try:
|
||||||
|
dp_list, leaf_stats = collect_decision_points(branch_tree, fields_dict)
|
||||||
|
cov_paths = [(pi[0], pi[1]) for pi in path_infos if isinstance(pi, (list, tuple)) and len(pi) >= 2]
|
||||||
|
mark_coverage(dp_list, leaf_stats, cov_paths, fields_dict)
|
||||||
|
if structure is not None:
|
||||||
|
structure['coverage'] = {
|
||||||
|
'decision_points': [{
|
||||||
|
'id': dp.id, 'kind': dp.kind,
|
||||||
|
'label': getattr(dp, 'label', '')[:60],
|
||||||
|
'branches': len(dp.branch_names),
|
||||||
|
'covered': len(dp.active_branches),
|
||||||
|
} for dp in dp_list],
|
||||||
|
'total': sum(len(dp.branch_names) for dp in dp_list),
|
||||||
|
'covered': sum(len(dp.active_branches) for dp in dp_list),
|
||||||
|
'pct': sum(len(dp.active_branches) for dp in dp_list) / max(sum(len(dp.branch_names) for dp in dp_list), 1) * 100,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
if structure is not None:
|
||||||
|
structure['coverage'] = {'error': str(e)[:80]}
|
||||||
|
|
||||||
if records:
|
if records:
|
||||||
import re as _re
|
import re as _re
|
||||||
proc_upper = (proc_div or "").upper()
|
proc_upper = (proc_div or "").upper()
|
||||||
|
|||||||
@@ -88,6 +88,21 @@ def parse_single_condition(text, fields=None):
|
|||||||
if re.match(r'^[A-Z][A-Z0-9-]*(?:\([^)]*\))?$', fn, re.IGNORECASE):
|
if re.match(r'^[A-Z][A-Z0-9-]*(?:\([^)]*\))?$', fn, re.IGNORECASE):
|
||||||
return (fn, '<>', 'Y')
|
return (fn, '<>', 'Y')
|
||||||
|
|
||||||
|
# NOT at start of condition: NOT WS-X > 50 → WS-X <= 50
|
||||||
|
# Strip leading NOT, parse the inner condition, invert the operator
|
||||||
|
if text.upper().startswith('NOT '):
|
||||||
|
inner = text[4:].strip()
|
||||||
|
inner_parsed = None
|
||||||
|
# Try standard regex on inner text
|
||||||
|
m_inner = re.match(r"^(\w[\w-]*(?:\s*\([^)]*\))?)\s*(>=|<=|<>|>|<|=)\s*(.+)$", inner)
|
||||||
|
if m_inner:
|
||||||
|
inv_op_map = {'=': '<>', '<>': '=', '>': '<=', '<': '>=', '>=': '<', '<=': '>'}
|
||||||
|
f = re.sub(r'\s*([(),])\s*', r'\1', m_inner.group(1))
|
||||||
|
op = m_inner.group(2)
|
||||||
|
val = m_inner.group(3).strip().strip("'").strip('"')
|
||||||
|
inv = inv_op_map.get(op, op)
|
||||||
|
return (f, inv, val)
|
||||||
|
|
||||||
# Normalize COBOL NOT-operators: X NOT = Y → X <> Y
|
# Normalize COBOL NOT-operators: X NOT = Y → X <> Y
|
||||||
normalized = text
|
normalized = text
|
||||||
not_map = [
|
not_map = [
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def collect_decision_points(node, fields, counter=None):
|
|||||||
counter[0] += 1
|
counter[0] += 1
|
||||||
dp = DecisionPoint(id=counter[0], kind='IF', label=node.condition,
|
dp = DecisionPoint(id=counter[0], kind='IF', label=node.condition,
|
||||||
branch_names=['T', 'F'])
|
branch_names=['T', 'F'])
|
||||||
simple = parse_single_condition(node.condition)
|
simple = parse_single_condition(node.condition, fields)
|
||||||
if simple and is_field(simple[0], fields):
|
if simple and is_field(simple[0], fields):
|
||||||
dp.parsed = simple
|
dp.parsed = simple
|
||||||
elif simple:
|
elif simple:
|
||||||
@@ -110,7 +110,7 @@ def collect_decision_points(node, fields, counter=None):
|
|||||||
dp = DecisionPoint(id=counter[0], kind='PERFORM',
|
dp = DecisionPoint(id=counter[0], kind='PERFORM',
|
||||||
label=node.condition or '',
|
label=node.condition or '',
|
||||||
branch_names=['Enter', 'Skip'])
|
branch_names=['Enter', 'Skip'])
|
||||||
simple = parse_single_condition(node.condition) if node.condition else None
|
simple = parse_single_condition(node.condition, fields) if node.condition else None
|
||||||
if simple and is_field(simple[0], fields):
|
if simple and is_field(simple[0], fields):
|
||||||
dp.parsed = simple
|
dp.parsed = simple
|
||||||
elif node.condition:
|
elif node.condition:
|
||||||
@@ -178,12 +178,17 @@ def _match_leaf(c, leaf):
|
|||||||
def _mark_if(dp, cons):
|
def _mark_if(dp, cons):
|
||||||
simple = getattr(dp, 'parsed', None)
|
simple = getattr(dp, 'parsed', None)
|
||||||
if simple:
|
if simple:
|
||||||
|
field, op, val = simple
|
||||||
|
inv_op = {'=': '<>', '<>': '=', '>': '<=', '<': '>=', '>=': '<', '<=': '>'}.get(op, op)
|
||||||
|
inv_simple = (field, inv_op, val)
|
||||||
for c in cons:
|
for c in cons:
|
||||||
if _match_constraint(c, simple):
|
if _match_constraint(c, simple):
|
||||||
if c[3]:
|
if c[3]:
|
||||||
dp.active_branches.add('T')
|
dp.active_branches.add('T')
|
||||||
else:
|
else:
|
||||||
dp.active_branches.add('F')
|
dp.active_branches.add('F')
|
||||||
|
elif _match_constraint(c, inv_simple):
|
||||||
|
dp.active_branches.add('F')
|
||||||
elif dp.cond_tree and dp.cond_leaves:
|
elif dp.cond_tree and dp.cond_leaves:
|
||||||
assignment = {}
|
assignment = {}
|
||||||
for leaf in dp.cond_leaves:
|
for leaf in dp.cond_leaves:
|
||||||
@@ -250,13 +255,27 @@ def _mark_eval(dp, cons, fields=None):
|
|||||||
if when_fields:
|
if when_fields:
|
||||||
dp.active_branches.add('OTHER')
|
dp.active_branches.add('OTHER')
|
||||||
return
|
return
|
||||||
|
matched_when = False
|
||||||
for c in cons:
|
for c in cons:
|
||||||
if c[0] == dp.label and c[1] == '=':
|
if c[0] == dp.label and c[1] == '=':
|
||||||
name = f"WHEN {c[2]}"
|
name = f"WHEN {c[2]}"
|
||||||
if name in dp.branch_names:
|
if name in dp.branch_names:
|
||||||
dp.active_branches.add(name)
|
dp.active_branches.add(name)
|
||||||
|
matched_when = True
|
||||||
|
elif c[0] == dp.label and c[1] == '<>':
|
||||||
|
pass # Inverted operator — skip (negation of a prior WHEN)
|
||||||
elif c[0] == dp.label and c[1] == 'not_in':
|
elif c[0] == dp.label and c[1] == 'not_in':
|
||||||
dp.active_branches.add('OTHER')
|
dp.active_branches.add('OTHER')
|
||||||
|
matched_when = True
|
||||||
|
# If all subject constraints are '<>' (negations) and no '=' matched,
|
||||||
|
# this path reaches OTHER (EVALUATE ... WHEN OTHER)
|
||||||
|
if not matched_when and 'OTHER' in dp.branch_names:
|
||||||
|
all_negs = all(c[1] == '<>' for c in cons if c[0] == dp.label)
|
||||||
|
if all_negs:
|
||||||
|
dp.active_branches.add('OTHER')
|
||||||
|
elif any(c[1] in ('>=', '<=') for c in cons if c[0] == dp.label):
|
||||||
|
# THRU-range OTHER detection
|
||||||
|
pass
|
||||||
thru_lows = {c[2] for c in cons if c[0] == dp.label and c[1] == '>=' and c[3]}
|
thru_lows = {c[2] for c in cons if c[0] == dp.label and c[1] == '>=' and c[3]}
|
||||||
thru_highs = {c[2] for c in cons if c[0] == dp.label and c[1] == '<=' and c[3]}
|
thru_highs = {c[2] for c in cons if c[0] == dp.label and c[1] == '<=' and c[3]}
|
||||||
if thru_lows or thru_highs:
|
if thru_lows or thru_highs:
|
||||||
@@ -309,12 +328,17 @@ def _mark_search(dp, cons, fields=None):
|
|||||||
def _mark_perform(dp, cons):
|
def _mark_perform(dp, cons):
|
||||||
simple = getattr(dp, 'parsed', None)
|
simple = getattr(dp, 'parsed', None)
|
||||||
if simple:
|
if simple:
|
||||||
|
field, op, val = simple
|
||||||
|
inv_op = {'=': '<>', '<>': '=', '>': '<=', '<': '>=', '>=': '<', '<=': '>'}.get(op, op)
|
||||||
|
inv_simple = (field, inv_op, val)
|
||||||
for c in cons:
|
for c in cons:
|
||||||
if _match_constraint(c, simple):
|
if _match_constraint(c, simple):
|
||||||
if c[3]:
|
if c[3]:
|
||||||
dp.active_branches.add('Skip')
|
dp.active_branches.add('Skip')
|
||||||
else:
|
else:
|
||||||
dp.active_branches.add('Enter')
|
dp.active_branches.add('Enter')
|
||||||
|
elif _match_constraint(c, inv_simple):
|
||||||
|
dp.active_branches.add('Enter')
|
||||||
elif dp.cond_tree and dp.cond_leaves:
|
elif dp.cond_tree and dp.cond_leaves:
|
||||||
assignment = {}
|
assignment = {}
|
||||||
for leaf in dp.cond_leaves:
|
for leaf in dp.cond_leaves:
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ def _make_path_for_branch(dp, branch_idx, fields):
|
|||||||
n_when = len(node.when_list)
|
n_when = len(node.when_list)
|
||||||
if branch_idx < n_when:
|
if branch_idx < n_when:
|
||||||
value, seq = node.when_list[branch_idx]
|
value, seq = node.when_list[branch_idx]
|
||||||
if is_field(node.subject, []):
|
if is_field(node.subject, fields):
|
||||||
constraints.append((node.subject, '=', value, True))
|
constraints.append((node.subject, '=', value, True))
|
||||||
prior_cases = [v for v, _ in node.when_list[:branch_idx]]
|
prior_cases = [v for v, _ in node.when_list[:branch_idx]]
|
||||||
for prior in prior_cases:
|
for prior in prior_cases:
|
||||||
@@ -212,7 +212,7 @@ def enum_paths(node, fields):
|
|||||||
if node.has_other:
|
if node.has_other:
|
||||||
other_cons = list(dp.get("access_constraints", []))
|
other_cons = list(dp.get("access_constraints", []))
|
||||||
for v, _ in node.when_list:
|
for v, _ in node.when_list:
|
||||||
if is_field(node.subject, []):
|
if is_field(node.subject, fields):
|
||||||
other_cons.append((node.subject, '<>', v, True))
|
other_cons.append((node.subject, '<>', v, True))
|
||||||
paths.append((other_cons, {}))
|
paths.append((other_cons, {}))
|
||||||
|
|
||||||
|
|||||||
@@ -99,13 +99,15 @@ def _convert_node(node: BranchNode, parent: BrSeq):
|
|||||||
|
|
||||||
if k == "PERFORM":
|
if k == "PERFORM":
|
||||||
cond = node.condition_text or ""
|
cond = node.condition_text or ""
|
||||||
u = cond.upper()
|
br_names = [b.upper() for b in node.branch_names] if node.branch_names else []
|
||||||
if 'VARYING' in u:
|
if any('VARY' in b for b in br_names):
|
||||||
br = BrPerform("varying", condition=cond)
|
br = BrPerform("varying", condition=cond)
|
||||||
elif 'UNTIL' in u:
|
elif any('SKIP' in b or 'ENTER' in b for b in br_names):
|
||||||
br = BrPerform("until", condition=cond)
|
br = BrPerform("until", condition=cond)
|
||||||
else:
|
elif any('TIMES' in b for b in br_names):
|
||||||
br = BrPerform("times", condition=cond)
|
br = BrPerform("times", condition=cond)
|
||||||
|
else:
|
||||||
|
br = BrPerform("until", condition=cond)
|
||||||
for c in node.children: _convert_node(c, br.body_seq)
|
for c in node.children: _convert_node(c, br.body_seq)
|
||||||
parent.add(br)
|
parent.add(br)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -405,9 +405,19 @@ def _add_or_merge(node: BranchNode, root: BranchNode):
|
|||||||
def _make_if_node(cond_text: str, line_no: int) -> BranchNode:
|
def _make_if_node(cond_text: str, line_no: int) -> BranchNode:
|
||||||
"""Create IF node with proper branch names from condition."""
|
"""Create IF node with proper branch names from condition."""
|
||||||
base_cond = cond_text.rstrip('.').strip()
|
base_cond = cond_text.rstrip('.').strip()
|
||||||
|
# Truncate condition at COBOL statement verbs (one-line IF)
|
||||||
|
_COBOL_VERBS = (
|
||||||
|
'DISPLAY', 'MOVE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'COMPUTE',
|
||||||
|
'STRING', 'UNSTRING', 'SET', 'INSPECT', 'INITIALIZE', 'CONTINUE',
|
||||||
|
'PERFORM', 'CALL', 'EXIT', 'GOBACK', 'STOP', 'THEN', 'ELSE',
|
||||||
|
'READ', 'WRITE', 'DELETE', 'REWRITE', 'ACCEPT', 'OPEN', 'CLOSE',
|
||||||
|
)
|
||||||
|
for verb in _COBOL_VERBS:
|
||||||
|
idx = base_cond.upper().find(f' {verb} ')
|
||||||
|
if idx >= 0:
|
||||||
|
base_cond = base_cond[:idx].strip()
|
||||||
|
break
|
||||||
# Parse condition for branch count
|
# Parse condition for branch count
|
||||||
# Single condition → 2 branches
|
|
||||||
# AND conditions → (N+1) branches
|
|
||||||
has_and = bool(re.search(r'\bAND\b', base_cond, re.IGNORECASE)
|
has_and = bool(re.search(r'\bAND\b', base_cond, re.IGNORECASE)
|
||||||
and not re.search(r'\bAND\b', base_cond.split('NOT')[1], re.IGNORECASE)
|
and not re.search(r'\bAND\b', base_cond.split('NOT')[1], re.IGNORECASE)
|
||||||
if 'NOT' in base_cond.upper() and len(base_cond.split('NOT')) > 1
|
if 'NOT' in base_cond.upper() and len(base_cond.split('NOT')) > 1
|
||||||
@@ -434,15 +444,29 @@ def _make_if_node(cond_text: str, line_no: int) -> BranchNode:
|
|||||||
def _make_perform_node(rest: str, line_no: int) -> BranchNode:
|
def _make_perform_node(rest: str, line_no: int) -> BranchNode:
|
||||||
"""Create PERFORM node."""
|
"""Create PERFORM node."""
|
||||||
upper = rest.upper()
|
upper = rest.upper()
|
||||||
|
# Truncate at COBOL verbs (one-line PERFORM: UNTIL cond BODY)
|
||||||
|
verb_list = (
|
||||||
|
'DISPLAY', 'MOVE', 'ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE', 'COMPUTE',
|
||||||
|
'STRING', 'UNSTRING', 'SET', 'INSPECT', 'INITIALIZE', 'CONTINUE',
|
||||||
|
'PERFORM', 'CALL', 'EXIT', 'GOBACK', 'STOP',
|
||||||
|
'READ', 'WRITE', 'DELETE', 'REWRITE', 'ACCEPT', 'OPEN', 'CLOSE',
|
||||||
|
)
|
||||||
|
cond_text = rest
|
||||||
|
for verb in verb_list:
|
||||||
|
idx = rest.upper().find(f' {verb} ')
|
||||||
|
if idx >= 0:
|
||||||
|
cond_text = rest[:idx].strip()
|
||||||
|
break
|
||||||
if upper.startswith('UNTIL'):
|
if upper.startswith('UNTIL'):
|
||||||
|
ctext = cond_text[5:].strip() if cond_text.upper().startswith('UNTIL') else cond_text
|
||||||
return BranchNode("PERFORM", branch_names=["ENTER", "SKIP"],
|
return BranchNode("PERFORM", branch_names=["ENTER", "SKIP"],
|
||||||
condition_text=rest[5:].strip(), source_line=line_no)
|
condition_text=ctext, source_line=line_no)
|
||||||
elif upper.startswith('VARYING'):
|
elif upper.startswith('VARYING'):
|
||||||
return BranchNode("PERFORM", branch_names=["VARY_ENTER", "VARY_EXIT"],
|
return BranchNode("PERFORM", branch_names=["VARY_ENTER", "VARY_EXIT"],
|
||||||
condition_text=rest, source_line=line_no)
|
condition_text=cond_text, source_line=line_no)
|
||||||
elif re.match(r'\bTIMES\b', upper):
|
elif re.match(r'\bTIMES\b', upper):
|
||||||
return BranchNode("PERFORM", branch_names=["TIMES_ENTER", "TIMES_EXIT"],
|
return BranchNode("PERFORM", branch_names=["TIMES_ENTER", "TIMES_EXIT"],
|
||||||
condition_text=rest, source_line=line_no)
|
condition_text=cond_text, source_line=line_no)
|
||||||
else:
|
else:
|
||||||
# Simple PERFORM paragraph-name — just a call, no branch
|
# Simple PERFORM paragraph-name — just a call, no branch
|
||||||
para_name = rest.split()[0].upper() if rest.split() else "?"
|
para_name = rest.split()[0].upper() if rest.split() else "?"
|
||||||
|
|||||||
Reference in New Issue
Block a user