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:
hangshuo652
2026-06-10 22:56:22 +08:00
parent 0730045e27
commit 7ac887c776
9 changed files with 509 additions and 1005 deletions
+97 -11
View File
@@ -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):