feat: SQL between/hostvar-key alignment, class-condition parsing, gcov merge across scenario runs
This commit is contained in:
+150
-27
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user