feat: complete INSPECT/SEARCH support, fix PERFORM/EVAL coverage marking
- Add INSPECT (TALLYING/REPLACING/CONVERTING) with BEFORE/AFTER INITIAL - Add SEARCH/SEARCH ALL with element-assignment path enumeration - Fix _mark_perform compound condition marking via evaluate_tree - Fix EVALUATE TRUE prior_false to collect all MC/DC false sets - Add impossible path filtering (Pass A.5) with trace-to-root conflict detection - Fix multi-line PERFORM VARYING parsing (VARYING/FROM/BY/UNTIL on separate lines) - Remove dead code: agents.py LLM parser (replaced by rule-based _BrParser) - 59 unit tests passing, 5 integration programs verified
This commit is contained in:
+97
-11
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from .models import BrSeq, BrIf, BrEval, BrPerform, CondLeaf
|
||||
from .models import BrSeq, BrIf, BrEval, BrPerform, BrSearch, CondLeaf
|
||||
from .cond import parse_single_condition, parse_compound_condition, is_field, collect_leaves, evaluate_tree
|
||||
|
||||
|
||||
@@ -83,6 +83,26 @@ def collect_decision_points(node, fields, counter=None):
|
||||
p, l = _walk_collect(node.other_seq, fields, counter)
|
||||
points.extend(p); all_leaves.extend(l)
|
||||
|
||||
elif isinstance(node, BrSearch):
|
||||
counter[0] += 1
|
||||
branch_names = []
|
||||
for cond_text, seq in node.when_list:
|
||||
branch_names.append(f'WHEN {cond_text[:40]}')
|
||||
if node.has_at_end:
|
||||
branch_names.append('AT END')
|
||||
dp = DecisionPoint(id=counter[0], kind='SEARCH',
|
||||
label=node.table_name, branch_names=branch_names)
|
||||
dp.when_list = node.when_list
|
||||
dp.cond_trees = node.cond_trees
|
||||
dp.has_other = node.has_at_end
|
||||
points.append(dp)
|
||||
for cond_text, seq in node.when_list:
|
||||
p, l = _walk_collect(seq, fields, counter)
|
||||
points.extend(p); all_leaves.extend(l)
|
||||
if node.has_at_end:
|
||||
p, l = _walk_collect(node.at_end_seq, fields, counter)
|
||||
points.extend(p); all_leaves.extend(l)
|
||||
|
||||
elif isinstance(node, BrPerform):
|
||||
if node.perf_type in ('until', 'para_until', 'varying', 'para_varying'):
|
||||
counter[0] += 1
|
||||
@@ -92,6 +112,13 @@ def collect_decision_points(node, fields, counter=None):
|
||||
simple = parse_single_condition(node.condition) if node.condition else None
|
||||
if simple and is_field(simple[0], fields):
|
||||
dp.parsed = simple
|
||||
elif node.condition:
|
||||
cond_tree = parse_compound_condition(node.condition, fields)
|
||||
if cond_tree:
|
||||
leaves = collect_leaves(cond_tree)
|
||||
if leaves:
|
||||
dp.cond_tree = cond_tree
|
||||
dp.cond_leaves = list(leaves)
|
||||
points.append(dp)
|
||||
p, l = _walk_collect(node.body_seq, fields, counter)
|
||||
points.extend(p); all_leaves.extend(l)
|
||||
@@ -116,9 +143,11 @@ def mark_coverage(decision_points, leaf_stats, branch_paths, fields):
|
||||
if dp.kind == 'IF':
|
||||
_mark_if(dp, cons)
|
||||
elif dp.kind == 'EVALUATE':
|
||||
_mark_eval(dp, cons)
|
||||
_mark_eval(dp, cons, fields)
|
||||
elif dp.kind == 'PERFORM':
|
||||
_mark_perform(dp, cons)
|
||||
elif dp.kind == 'SEARCH':
|
||||
_mark_search(dp, cons, fields)
|
||||
for leaf in leaf_stats:
|
||||
for c in cons:
|
||||
if _match_leaf(c, leaf):
|
||||
@@ -128,7 +157,7 @@ def mark_coverage(decision_points, leaf_stats, branch_paths, fields):
|
||||
leaf.covered_false = True
|
||||
|
||||
for dp in decision_points:
|
||||
_infer_implied(dp)
|
||||
dp.implied_branches = set(dp.active_branches)
|
||||
|
||||
|
||||
def _match_constraint(c, parsed):
|
||||
@@ -180,18 +209,20 @@ def _mark_if(dp, cons):
|
||||
dp.active_branches.add('T' if c[3] else 'F')
|
||||
|
||||
|
||||
def _mark_eval(dp, cons):
|
||||
def _mark_eval(dp, cons, fields=None):
|
||||
if dp.label == 'TRUE':
|
||||
matched = False
|
||||
for when_val, _ in dp.when_list:
|
||||
parsed = parse_single_condition(when_val)
|
||||
parsed = parse_single_condition(when_val, fields)
|
||||
if parsed:
|
||||
for c in cons:
|
||||
if _match_constraint(c, parsed):
|
||||
if _match_constraint(c, parsed) and c[3]:
|
||||
name = f"WHEN {when_val}"
|
||||
if name in dp.branch_names:
|
||||
dp.active_branches.add(name)
|
||||
matched = True
|
||||
else:
|
||||
cond_tree = parse_compound_condition(when_val)
|
||||
cond_tree = parse_compound_condition(when_val, fields)
|
||||
if cond_tree and not isinstance(cond_tree, CondLeaf):
|
||||
leaves = list(collect_leaves(cond_tree))
|
||||
assignment = {}
|
||||
@@ -205,6 +236,15 @@ def _mark_eval(dp, cons):
|
||||
name = f"WHEN {when_val}"
|
||||
if name in dp.branch_names:
|
||||
dp.active_branches.add(name)
|
||||
matched = True
|
||||
if not matched and 'OTHER' in dp.branch_names:
|
||||
when_fields = set()
|
||||
for when_val, _ in dp.when_list:
|
||||
for c in cons:
|
||||
if c[0] in when_val:
|
||||
when_fields.add(c[0])
|
||||
if when_fields:
|
||||
dp.active_branches.add('OTHER')
|
||||
return
|
||||
for c in cons:
|
||||
if c[0] == dp.label and c[1] == '=':
|
||||
@@ -215,6 +255,44 @@ def _mark_eval(dp, cons):
|
||||
dp.active_branches.add('OTHER')
|
||||
|
||||
|
||||
def _mark_search(dp, cons, fields=None):
|
||||
branch_masks = [False] * len(dp.branch_names)
|
||||
for i, (cond_text, body_seq) in enumerate(dp.when_list):
|
||||
cond_tree = dp.cond_trees[i] if i < len(dp.cond_trees) else None
|
||||
if not cond_tree:
|
||||
continue
|
||||
if isinstance(cond_tree, CondLeaf):
|
||||
for c in cons:
|
||||
if len(c) == 4:
|
||||
base_c = re.sub(r'\s*\(.*?\)\s*$', '', c[0])
|
||||
base_cond = re.sub(r'\s*\(.*?\)\s*$', '', cond_tree.field)
|
||||
if base_c == base_cond and c[1] == cond_tree.op \
|
||||
and str(c[2]) == str(cond_tree.value) and c[3]:
|
||||
branch_masks[i] = True
|
||||
break
|
||||
else:
|
||||
leaves = list(collect_leaves(cond_tree))
|
||||
assignment = {}
|
||||
for leaf in leaves:
|
||||
for c in cons:
|
||||
if len(c) == 4:
|
||||
base_c = re.sub(r'\s*\(.*?\)\s*$', '', c[0])
|
||||
base_l = re.sub(r'\s*\(.*?\)\s*$', '', leaf.field)
|
||||
if base_c == base_l and c[1] == leaf.op and str(c[2]) == str(leaf.value):
|
||||
assignment[leaf] = c[3]
|
||||
break
|
||||
if len(assignment) == len(leaves):
|
||||
if evaluate_tree(cond_tree, assignment):
|
||||
branch_masks[i] = True
|
||||
if dp.has_other:
|
||||
at_end_idx = len(dp.branch_names) - 1
|
||||
if not any(branch_masks[:at_end_idx]):
|
||||
branch_masks[at_end_idx] = True
|
||||
for i, m in enumerate(branch_masks):
|
||||
if m:
|
||||
dp.active_branches.add(dp.branch_names[i])
|
||||
|
||||
|
||||
def _mark_perform(dp, cons):
|
||||
simple = getattr(dp, 'parsed', None)
|
||||
if simple:
|
||||
@@ -224,6 +302,18 @@ def _mark_perform(dp, cons):
|
||||
dp.active_branches.add('Skip')
|
||||
else:
|
||||
dp.active_branches.add('Enter')
|
||||
elif dp.cond_tree and dp.cond_leaves:
|
||||
assignment = {}
|
||||
for leaf in dp.cond_leaves:
|
||||
for c in cons:
|
||||
if _match_leaf(c, leaf):
|
||||
assignment[leaf] = c[3]
|
||||
break
|
||||
if len(assignment) == len(dp.cond_leaves):
|
||||
if evaluate_tree(dp.cond_tree, assignment):
|
||||
dp.active_branches.add('Skip')
|
||||
else:
|
||||
dp.active_branches.add('Enter')
|
||||
else:
|
||||
for c in cons:
|
||||
if c[0] == dp.label or any(c[0] == f for f in _get_fields_in_cond(dp.label)):
|
||||
@@ -237,10 +327,6 @@ def _get_fields_in_cond(cond_text):
|
||||
return re.findall(r'[A-Z][A-Z0-9-]*', cond_text.upper())
|
||||
|
||||
|
||||
def _infer_implied(dp):
|
||||
dp.implied_branches.update(dp.active_branches)
|
||||
|
||||
|
||||
# ── 行号定位(基于原始源文本)──
|
||||
|
||||
def locate_decision_lines(decision_points, raw_source):
|
||||
|
||||
Reference in New Issue
Block a user