feat: SQL between/hostvar-key alignment, class-condition parsing, gcov merge across scenario runs

This commit is contained in:
hangshuo652
2026-08-09 17:43:00 +08:00
parent f331c8fa2a
commit 273a3f8211
31 changed files with 3789 additions and 272 deletions
+150 -27
View File
@@ -35,6 +35,9 @@ def _invert_condition(parsed):
"""Invert a parsed condition (True ↔ False)."""
if parsed is None:
return None
if len(parsed) == 4:
# class condition (field, 'IS', CLASS, want) → flip want
return (parsed[0], parsed[1], parsed[2], not parsed[3])
field, op, val = parsed
inv_op = {'=': '<>', '<>': '=', '>': '<=', '<': '>=', '>=': '<', '<=': '>'}.get(op, op)
return (field, inv_op, val)
@@ -78,13 +81,43 @@ def _collect_all_dps(node, fields, path_cons=None, path_assign=None, depth=0, _c
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))
if len(parsed) == 4:
field, op, val, base_want = parsed
t_cons.append((field, op, val, base_want))
f_cons.append((field, op, val, not base_want))
else:
field, op, val = parsed
t_cons.append((field, op, val, True))
f_cons.append((field, op, val, False))
else:
# Synthetic constraint for coverage matching
t_cons.append(("__DP", str(dp_id), "T", True))
f_cons.append(("__DP", str(dp_id), "F", True))
# Generate MC/DC leaf-level constraints for compound conditions
cond_tree = parse_compound_condition(node.condition, fields)
if cond_tree:
sets = mcdc_sets(cond_tree, fields)
if sets:
t_set = f_set = None
for c, decision in sets:
if decision and t_set is None:
t_set = c
elif not decision and f_set is None:
f_set = c
if t_set is not None and f_set is not None:
break
if t_set:
t_cons.extend(t_set)
else:
t_cons.append(("__DP", str(dp_id), "T", True))
if f_set:
f_cons.extend(f_set)
else:
f_cons.append(("__DP", str(dp_id), "F", True))
dp["false_idx"] = 1
else:
t_cons.append(("__DP", str(dp_id), "T", True))
f_cons.append(("__DP", str(dp_id), "F", True))
else:
t_cons.append(("__DP", str(dp_id), "T", True))
f_cons.append(("__DP", str(dp_id), "F", True))
result.extend(_collect_all_dps(node.true_seq, fields, t_cons, path_assign, depth + 1, _counter))
result.extend(_collect_all_dps(node.false_seq, fields, f_cons, path_assign, depth + 1, _counter))
@@ -128,8 +161,12 @@ def _collect_all_dps(node, fields, path_cons=None, path_assign=None, depth=0, _c
}
result.append(dp)
if parsed:
field, op, val = parsed
body_cons = list(path_cons) + [(field, op, val, False)]
if len(parsed) == 4:
field, op, val, base_want = parsed
body_cons = list(path_cons) + [(field, op, val, not base_want)]
else:
field, op, val = parsed
body_cons = list(path_cons) + [(field, op, val, False)]
else:
# Synthetic constraint for coverage matching
body_cons = list(path_cons) + [("__DP", str(dp_id), "ENTER", True)]
@@ -142,8 +179,10 @@ def _collect_all_dps(node, fields, path_cons=None, path_assign=None, depth=0, _c
result.extend(_collect_all_dps(child, fields, path_cons, path_assign, depth, _counter))
elif isinstance(node, BrSearch):
dp_id = _counter[0]
_counter[0] += 1
dp = {
"node": node, "kind": "SEARCH",
"node": node, "kind": "SEARCH", "id": dp_id,
"access_constraints": list(path_cons),
}
result.append(dp)
@@ -165,17 +204,37 @@ def _make_path_for_branch(dp, branch_idx, fields):
dp_id = dp.get("id", 0)
want_true = (branch_idx == dp.get("true_idx", 0))
if parsed is None:
# Use synthetic __DP constraint for coverage matching
label = "T" if want_true else "F"
constraints.append(("__DP", str(dp_id), label, True))
# Generate MC/DC leaf-level constraints for compound conditions
cond_tree = parse_compound_condition(dp["node"].condition, fields)
if cond_tree:
sets = mcdc_sets(cond_tree, fields)
if sets:
for c, decision in sets:
if decision == want_true:
constraints.extend(c)
break
else:
label = "T" if want_true else "F"
constraints.append(("__DP", str(dp_id), label, True))
else:
label = "T" if want_true else "F"
constraints.append(("__DP", str(dp_id), label, True))
else:
label = "T" if want_true else "F"
constraints.append(("__DP", str(dp_id), label, True))
node = dp["node"]
body_seq = node.true_seq if branch_idx == 0 else node.false_seq
else:
field, op, val = parsed
if not want_true:
field2, op2, val2 = _invert_condition(parsed)
field, op, val = field2, op2, val2
constraints.append((field, op, val, True))
if len(parsed) == 4:
field, op, val, base_want = parsed
leaf_want = base_want if want_true else (not base_want)
constraints.append((field, op, val, leaf_want))
else:
field, op, val = parsed
if not want_true:
field2, op2, val2 = _invert_condition(parsed)
field, op, val = field2, op2, val2
constraints.append((field, op, val, True))
node = dp["node"]
body_seq = node.true_seq if branch_idx == 0 else node.false_seq
return (constraints, {})
@@ -215,11 +274,38 @@ def _make_path_for_branch(dp, branch_idx, fields):
label = "ENTER" if branch_idx == 0 else "SKIP"
constraints.append(("__DP", str(dp_id), label, True))
return (constraints, {})
field, op, val = parsed
if branch_idx == 0:
constraints.append((field, op, val, False))
if len(parsed) == 4:
field, op, val, base_want = parsed
leaf_want = (not base_want) if branch_idx == 0 else base_want
constraints.append((field, op, val, leaf_want))
else:
constraints.append((field, op, val, True))
field, op, val = parsed
if branch_idx == 0:
constraints.append((field, op, val, False))
else:
constraints.append((field, op, val, True))
return (constraints, {})
if kind == "SEARCH":
node = dp["node"]
dp_id = dp.get("id", 0)
n_when = len(node.when_list)
if branch_idx < n_when:
cond_text, _ = node.when_list[branch_idx]
cond_tree = node.cond_trees[branch_idx] if branch_idx < len(node.cond_trees) else None
if cond_tree and isinstance(cond_tree, CondLeaf):
base = re.sub(r"\s*\(.*?\)\s*$", "", cond_tree.field)
elem_key = f"{base}({branch_idx + 1})"
subj = cond_tree.value
if is_field(subj, fields):
# Field-to-field: constrain subject against the table element
constraints.append((subj, cond_tree.op, elem_key, True))
else:
constraints.append((elem_key, cond_tree.op, str(subj).rstrip(), True))
else:
constraints.append(("__DP", str(dp_id), "W%d" % branch_idx, True))
return (constraints, {})
# AT END branch (branch_idx == n_when): leave access constraints only
return (constraints, {})
return ([], {})
@@ -245,12 +331,39 @@ def enum_paths(node, fields):
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 1, fields)
if true_path:
paths.append(true_path)
if false_path:
paths.append(false_path)
parsed = dp.get("parsed")
if parsed is None:
# Compound condition: add ALL MC/DC paths (one per leaf)
cond_tree = parse_compound_condition(dp["node"].condition, fields)
if cond_tree:
sets = mcdc_sets(cond_tree, fields)
if sets:
seen = set()
for c, decision in sets:
key = frozenset(c)
if key in seen:
continue
seen.add(key)
new_cons = list(dp.get("access_constraints", []))
new_cons.extend(c)
paths.append((new_cons, {}))
else:
true_path = _make_path_for_branch(dp, dp.get("true_idx", 0), fields)
if true_path: paths.append(true_path)
false_path = _make_path_for_branch(dp, dp.get("false_idx", 1) if dp.get("false_idx") is not None else 1, fields)
if false_path: paths.append(false_path)
else:
true_path = _make_path_for_branch(dp, dp.get("true_idx", 0), fields)
if true_path: paths.append(true_path)
false_path = _make_path_for_branch(dp, dp.get("false_idx", 1) if dp.get("false_idx") is not None else 1, fields)
if false_path: paths.append(false_path)
else:
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 1, fields)
if true_path:
paths.append(true_path)
if false_path:
paths.append(false_path)
elif kind == "EVALUATE":
node = dp["node"]
@@ -275,6 +388,16 @@ def enum_paths(node, fields):
if enter_path: paths.append(enter_path)
if skip_path: paths.append(skip_path)
elif kind == "SEARCH":
node = dp["node"]
n_when = len(node.when_list)
for i in range(n_when):
wp = _make_path_for_branch(dp, i, fields)
if wp: paths.append(wp)
if node.has_at_end:
at = _make_path_for_branch(dp, n_when, fields)
if at: paths.append(at)
if len(paths) >= MAX_PATH:
paths = paths[:MAX_PATH]
break