feat: Phase 2 complete — 13 Phases of COBOL type classification and test benchmark

P0.6: gcov infrastructure
P1: extract_structure output expansion (11 new feature fields)
P2: Confusion group rule engine (8 pairs + contradiction + backtrack)
P3: 4-factor confidence calculation + quality gate update
P4: 33+2 COBOL program type test samples (22 files, 7 categories)
P5: parametrized/ test data generation engine
P6: japanese_data.py lookup tables
P7-10: Type-specific test suites (~159 parametrized tests)
P11: Full classification pipeline (classify_program) + orchestrator integration
P12: Documentation (module-interfaces, test-plan v3.0, coverage-matrix)

Architecture decisions:
- classification_pipeline/ merged to hina/pipeline/
- parametrized/ as independent module
- japanese_data.py as root-level file
- hina/__all__ only exports classify_program()

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hangshuo652
2026-06-19 23:51:55 +08:00
parent 63b5284715
commit bc1d56d1a4
129 changed files with 19378 additions and 261 deletions
+843
View File
@@ -0,0 +1,843 @@
"""CO-DP-01~13: cobol_testgen cond 模块 — 深度条件测试 (MC/DC, 嵌套, 88-level, 性能)"""
import sys, os, time
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from cobol_testgen.cond import (
parse_single_condition, parse_compound_condition,
collect_leaves, evaluate_tree, mcdc_sets, satisfying_value,
)
from cobol_testgen.models import CondLeaf, CondAnd, CondOr, CondNot
# ══════════════════════════════════════════════════════════════════
# CO-DP-01: 3-layer nested AND/OR
# ══════════════════════════════════════════════════════════════════
def test_deep_nested_and_or_parse():
"""CO-DP-01: (A > 0 AND B < 5) OR (C = 1 AND NOT D > 10) — 3层嵌套解析"""
text = "(A > 0 AND B < 5) OR (C = 1 AND NOT D > 10)"
tree = parse_compound_condition(text)
assert tree is not None
# Root is CondOr
assert isinstance(tree, CondOr), f"Expected CondOr, got {type(tree).__name__}"
# Left leg: (A > 0 AND B < 5) → CondAnd
left = tree.left
assert isinstance(left, CondAnd), f"Left child expected CondAnd, got {type(left).__name__}"
assert isinstance(left.left, CondLeaf)
assert left.left.field == "A"
assert left.left.op == ">"
assert left.left.value == "0"
assert isinstance(left.right, CondLeaf)
assert left.right.field == "B"
assert left.right.op == "<"
assert left.right.value == "5"
# Right leg: (C = 1 AND NOT D > 10) → CondAnd(CondLeaf, CondNot(CondLeaf))
right = tree.right
assert isinstance(right, CondAnd), f"Right child expected CondAnd, got {type(right).__name__}"
assert isinstance(right.left, CondLeaf)
assert right.left.field == "C"
assert right.left.op == "="
assert right.left.value == "1"
assert isinstance(right.right, CondNot), f"Expected CondNot wrapping D, got {type(right.right).__name__}"
assert isinstance(right.right.child, CondLeaf)
assert right.right.child.field == "D"
assert right.right.child.op == ">"
assert right.right.child.value == "10"
# collect_leaves should return 4 leaves (NOT's child is still a leaf)
leaves = collect_leaves(tree)
assert len(leaves) == 4, f"Expected 4 leaves, got {len(leaves)}"
fields = [l.field for l in leaves]
assert "A" in fields and "B" in fields and "C" in fields and "D" in fields
def test_deep_nested_and_or_evaluate():
"""CO-DP-01b: evaluate_tree for 3-layer nested AND/OR"""
text = "(A > 0 AND B < 5) OR (C = 1 AND NOT D > 10)"
tree = parse_compound_condition(text)
leaves = collect_leaves(tree)
# Map field names to leaf objects
leaf_map = {l.field: l for l in leaves}
a = leaf_map["A"]
b = leaf_map["B"]
c = leaf_map["C"]
d = leaf_map["D"]
# (T AND T) OR (F AND NOT F) = T OR (F AND T) = T OR F = T
assert evaluate_tree(tree, {a: True, b: True, c: False, d: False}) is True
# (F AND T) OR (F AND NOT F) = F OR (F AND T) = F OR F = F
assert evaluate_tree(tree, {a: False, b: True, c: False, d: False}) is False
# (F AND F) OR (T AND NOT F) = F OR (T AND T) = F OR T = T
assert evaluate_tree(tree, {a: False, b: False, c: True, d: False}) is True
# (T AND T) OR (F AND NOT T) = T OR (F AND F) = T OR F = T
assert evaluate_tree(tree, {a: True, b: True, c: False, d: True}) is True
# (F AND F) OR (F AND NOT F) = F OR (F AND T) = F OR F = F
assert evaluate_tree(tree, {a: False, b: False, c: False, d: False}) is False
# (T AND T) OR (T AND NOT F) = T OR (T AND T) = T OR T = T
assert evaluate_tree(tree, {a: True, b: True, c: True, d: False}) is True
# (F AND T) OR (T AND NOT T) = F OR (T AND F) = F OR F = F
assert evaluate_tree(tree, {a: False, b: True, c: True, d: True}) is False
def test_deep_nested_and_or_mcdc():
"""CO-DP-01c: mcdc_sets for 3-layer nested AND/OR — should find >= 5 sets"""
text = "(A > 0 AND B < 5) OR (C = 1 AND NOT D > 10)"
tree = parse_compound_condition(text)
sets = mcdc_sets(tree)
assert sets is not None, "mcdc_sets should not return None for 4-leaf compound tree"
# With 4 leaves we expect at least 5 unique constraint sets
# (one "base" case + one showing independent effect per leaf at minimum)
assert len(sets) >= 5, f"Expected >= 5 MC/DC sets, got {len(sets)}"
assert len(sets) <= 8, f"Expected <= 8 MC/DC sets for 4-leaf, got {len(sets)}"
# Verify both True and False decision outcomes are present
decisions = set(d for _, d in sets)
assert True in decisions, "Should have True decision outcomes"
assert False in decisions, "Should have False decision outcomes"
# Verify all 4 leaves have their field referenced in constraints
all_field_names = set()
for constraints, _ in sets:
for c in constraints:
all_field_names.add(c[0])
for fname in ("A", "B", "C", "D"):
assert fname in all_field_names, f"Leaf {fname} not found in any MC/DC constraint"
# ══════════════════════════════════════════════════════════════════
# CO-DP-02: 88-level multi-value
# ══════════════════════════════════════════════════════════════════
def test_88_multi_value_resolve():
"""CO-DP-02: 88-level with multiple VALUES 'A' 'B' 'C' resolves to first value"""
fields = [
{
"is_88": True,
"name": "STATUS-VALID",
"parent": "WS-STATUS",
"value": "A",
"values": ["A", "B", "C"],
}
]
r = parse_single_condition("STATUS-VALID", fields)
assert r is not None, "88-level multi-value should resolve"
assert r[0] == "WS-STATUS", f"Expected parent WS-STATUS, got {r[0]}"
assert r[1] == "=", f"Expected operator '=', got {r[1]}"
# Current implementation uses f.get('value') which is the first value
assert r[2] == "A", f"Expected value 'A' (first in multi-value), got {r[2]}"
def test_88_multi_value_compound_parse():
"""CO-DP-02b: 88-level multi-value within compound expression"""
fields = [
{
"is_88": True,
"name": "STATUS-VALID",
"parent": "WS-STATUS",
"value": "A",
"values": ["A", "B", "C"],
},
{
"is_88": True,
"name": "AMOUNT-LARGE",
"parent": "WS-AMOUNT",
"value": "100",
},
]
tree = parse_compound_condition("STATUS-VALID AND AMOUNT-LARGE", fields)
assert tree is not None
assert isinstance(tree, CondAnd)
# Left: 88-level resolved to CondLeaf
assert isinstance(tree.left, CondLeaf)
assert tree.left.field == "WS-STATUS"
assert tree.left.value == "A"
assert tree.left.op == "="
# Right: 88-level resolved to CondLeaf
assert isinstance(tree.right, CondLeaf)
assert tree.right.field == "WS-AMOUNT"
assert tree.right.value == "100"
assert tree.right.op == "="
def test_88_multi_value_no_single_value():
"""CO-DP-02c: 88-level with only values[] (no single 'value') — current behavior"""
# Simulate a field that has values list but no single value key
fields = [
{
"is_88": True,
"name": "COLOR-RED",
"parent": "WS-COLOR",
"value": "RED",
}
]
r = parse_single_condition("COLOR-RED", fields)
assert r is not None
assert r[2] == "RED"
# Without a 'value' key, parse_single_condition returns empty string
fields_no_val = [
{
"is_88": True,
"name": "COLOR-RED",
"parent": "WS-COLOR",
"values": ["RED"],
}
]
# 'value' key missing entirely → f.get('value', '') returns ''
r2 = parse_single_condition("COLOR-RED", fields_no_val)
assert r2 is not None
assert r2[2] == "", f"Without value key, expected '', got '{r2[2]}'"
# ══════════════════════════════════════════════════════════════════
# CO-DP-03: Arithmetic expressions in conditions
# ══════════════════════════════════════════════════════════════════
def test_arithmetic_expr_add_mul():
"""CO-DP-03: A + B > C * 2 — arithmetic expression as leaf"""
r = parse_single_condition("A + B > C * 2")
assert r is not None, "Arithmetic expression A + B > C * 2 should parse"
# The field part is the whole left expression
assert "A + B" in r[0] or r[0] == "A + B", f"Expected left expr, got {r[0]}"
assert r[1] == ">", f"Expected operator '>', got {r[1]}"
assert "C * 2" in r[2] or r[2] == "C * 2", f"Expected right expr 'C * 2', got {r[2]}"
def test_arithmetic_expr_sub_eq():
"""CO-DP-03b: A - B = 5 — arithmetic expression with subtraction"""
r = parse_single_condition("A - B = 5")
assert r is not None, "Arithmetic expression A - B = 5 should parse"
assert r[1] == "=", f"Expected operator '=', got {r[1]}"
assert r[2] == "5", f"Expected value '5', got {r[2]}"
def test_arithmetic_expr_in_compound():
"""CO-DP-03c: Arithmetic expr in compound: X + Y > 10 OR A = 1"""
tree = parse_compound_condition("X + Y > 10 OR A = 1")
assert tree is not None
assert isinstance(tree, CondOr), f"Expected CondOr, got {type(tree).__name__}"
assert isinstance(tree.left, CondLeaf)
assert isinstance(tree.right, CondLeaf)
# Left leaf is the arithmetic expression
assert "X + Y" in tree.left.field or tree.left.field == "X + Y", \
f"Expected left expr 'X + Y', got '{tree.left.field}'"
assert tree.left.op == ">"
assert tree.right.field == "A"
assert tree.right.value == "1"
def test_arithmetic_expr_div():
"""CO-DP-03d: X / Y = 2 — division in condition"""
r = parse_single_condition("X / Y = 2")
assert r is not None, "X / Y = 2 should parse"
assert r[1] == "="
assert r[2] == "2"
# ══════════════════════════════════════════════════════════════════
# CO-DP-04: satisfying_value for ALL operators
# ══════════════════════════════════════════════════════════════════
def test_satisfying_value_numeric_all():
"""CO-DP-04: satisfying_value numeric — all 6 operators × want_true/False"""
info = {"type": "numeric", "digits": 7, "decimal": 0}
# --- want_true=True ---
# > should return value + 1
gt = satisfying_value(info, ">", "100", want_true=True)
assert int(gt) > 100, f"> want_true=True: expected >100, got {gt}"
# >= should return same (pass through)
ge = satisfying_value(info, ">=", "100", want_true=True)
assert int(ge) >= 100, f">= want_true=True: expected >=100, got {ge}"
# = should return same (pass through)
eq = satisfying_value(info, "=", "100", want_true=True)
assert int(eq) == 100, f"= want_true=True: expected 100, got {eq}"
# < should return value - 1
lt = satisfying_value(info, "<", "100", want_true=True)
assert int(lt) < 100, f"< want_true=True: expected <100, got {lt}"
# <= should return same (pass through)
le = satisfying_value(info, "<=", "100", want_true=True)
assert int(le) <= 100, f"<= want_true=True: expected <=100, got {le}"
# <> should return different value
ne = satisfying_value(info, "<>", "100", want_true=True)
assert int(ne) != 100, f"<> want_true=True: expected !=100, got {ne}"
# --- want_true=False ---
# > False → should set to 0 (so that condition is false)
gt_f = satisfying_value(info, ">", "100", want_true=False)
assert not (int(gt_f) > 100), f"> want_true=False: expected <=100, got {gt_f}"
# >= False → should set to 0
ge_f = satisfying_value(info, ">=", "100", want_true=False)
# Since >= is False, we want val < 100. Setting to 0 achieves this.
assert int(ge_f) < 100, f">= want_true=False: expected <100, got {ge_f}"
# = False → should return different value
eq_f = satisfying_value(info, "=", "100", want_true=False)
assert int(eq_f) != 100, f"= want_true=False: expected !=100, got {eq_f}"
# < False → should return same value (pass through)
lt_f = satisfying_value(info, "<", "100", want_true=False)
# want_true=False for < means we want >=, so keeping it at 100 works
assert int(lt_f) >= 100, f"< want_true=False: expected >=100, got {lt_f}"
# <= False → should return val + 1 (so condition fails because val > target)
le_f = satisfying_value(info, "<=", "100", want_true=False)
assert int(le_f) > 100, f"<= want_true=False: expected >100, got {le_f}"
# <> False → should return same value (pass through)
ne_f = satisfying_value(info, "<>", "100", want_true=False)
assert int(ne_f) == 100, f"<> want_true=False: expected 100, got {ne_f}"
def test_satisfying_value_alpha():
"""CO-DP-04b: satisfying_value alphanumeric — = and <> operators"""
info = {"type": "alphanumeric", "length": 3}
# = want_true=True → same letter repeated
eq = satisfying_value(info, "=", "ABC", want_true=True)
assert eq == "AAA", f"= want_true=True alpha: expected 'AAA', got '{eq}'"
# = want_true=False → different letter
eq_f = satisfying_value(info, "=", "ABC", want_true=False)
assert eq_f != "AAA", f"= want_true=False alpha: expected different from 'AAA', got '{eq_f}'"
assert len(eq_f) == 3
# <> want_true=True → different letter
ne = satisfying_value(info, "<>", "ABC", want_true=True)
assert ne != "AAA", f"<> want_true=True alpha: expected different from 'AAA', got '{ne}'"
assert len(ne) == 3
# <> want_true=False → same letter
ne_f = satisfying_value(info, "<>", "ABC", want_true=False)
assert ne_f == "AAA", f"<> want_true=False alpha: expected 'AAA', got '{ne_f}'"
def test_satisfying_value_alpha_single_char():
"""CO-DP-04c: satisfying_value alphabetic — single char values"""
info = {"type": "alphabetic", "length": 1}
eq = satisfying_value(info, "=", "Y", want_true=True)
assert eq == "Y", f"= want_true=True alpha(1): expected 'Y', got '{eq}'"
eq_f = satisfying_value(info, "=", "Y", want_true=False)
assert eq_f != "Y", f"= want_true=False alpha(1): expected not 'Y', got '{eq_f}'"
def test_satisfying_value_numeric_edge():
"""CO-DP-04d: satisfying_value numeric — edge cases (negative, decimal)"""
# Negative value
info_neg = {"type": "numeric", "digits": 5, "decimal": 0}
# > negative: should increment
gt = satisfying_value(info_neg, ">", "-5", want_true=True)
assert int(gt) > -5, f"> negative want_true=True: expected >-5, got {gt}"
# Decimal PIC (digits=5, decimal=2 means total 7, with 2 decimal places)
info_dec = {"type": "numeric", "digits": 5, "decimal": 2}
val = satisfying_value(info_dec, ">", "100", want_true=True)
# The value has 5 integer digits + 2 decimal digits = 7 total chars
# No dot, just concatenation: e.g., "0010100" means 00101.00
assert len(val) == 7, f"Expected 7 chars (5 int + 2 dec), got '{val}' (len={len(val)})"
# Verify > 100: the integer part (first 5 chars) should be > 100
int_part = int(val[:5])
dec_part = val[5:]
assert int_part > 100 or (int_part == 100 and int(dec_part) > 0), \
f"Expected > 100, got int_part={int_part}, dec={dec_part}"
def test_satisfying_value_figurative():
"""CO-DP-04e: satisfying_value — COBOL figurative constant fallback"""
# When value is non-numeric like 'ZERO', the float conversion may fail
info = {"type": "numeric", "digits": 5, "decimal": 0}
# non-numeric chars in value → val_float conversion fails → val_int = 0
result = satisfying_value(info, ">", "ABC", want_true=True)
assert result is not None
# val_int starts at 0, then increments by 1 for >=, so becomes 1
assert result == "00001", f"Expected '00001' (0+1), got '{result}'"
# ══════════════════════════════════════════════════════════════════
# CO-DP-05: Performance — 50-condition compound parse < 1s
# ══════════════════════════════════════════════════════════════════
def test_performance_50_and_conditions():
"""CO-DP-05: 50-condition AND chain parses in under 1 second"""
conditions = " AND ".join(f"A{i} > 0" for i in range(50))
start = time.time()
tree = parse_compound_condition(conditions)
elapsed = time.time() - start
assert elapsed < 1.0, \
f"Parsing 50 AND conditions took {elapsed:.3f}s (limit: 1.0s)"
assert tree is not None, "50-condition AND tree should not be None"
# Should be a deeply-nested CondAnd tree
leaves = collect_leaves(tree)
assert len(leaves) == 50, f"Expected 50 leaves, got {len(leaves)}"
# Verify field names are preserved
fields_found = {l.field for l in leaves}
for i in range(50):
assert f"A{i}" in fields_found, f"Field A{i} missing from parsed tree"
def test_performance_50_mixed_conditions():
"""CO-DP-05b: 50-condition mixed AND/OR with parens parses in under 1s"""
# Build: (A0 > 0 OR A1 > 0) AND (A2 > 0 OR A3 > 0) AND ...
pairs = []
for i in range(0, 50, 2):
pairs.append(f"(A{i} > 0 OR A{i+1} > 0)")
conditions = " AND ".join(pairs)
start = time.time()
tree = parse_compound_condition(conditions)
elapsed = time.time() - start
assert elapsed < 1.0, \
f"Parsing 50 mixed conditions took {elapsed:.3f}s (limit: 1.0s)"
assert tree is not None, "50-condition mixed tree should not be None"
leaves = collect_leaves(tree)
assert len(leaves) == 50, f"Expected 50 leaves, got {len(leaves)}"
# ══════════════════════════════════════════════════════════════════
# CO-DP-06: CondNot(CondNot(leaf)) — double negation
# ══════════════════════════════════════════════════════════════════
def test_double_negation_parse():
"""CO-DP-06: NOT NOT A > 0 → CondNot(CondNot(CondLeaf)) — no simplification"""
tree = parse_compound_condition("NOT NOT A > 0")
assert tree is not None
assert isinstance(tree, CondNot), f"Outer: expected CondNot, got {type(tree).__name__}"
assert isinstance(tree.child, CondNot), \
f"Inner: expected CondNot, got {type(tree.child).__name__}"
assert isinstance(tree.child.child, CondLeaf), \
f"Leaf: expected CondLeaf, got {type(tree.child.child).__name__}"
assert tree.child.child.field == "A"
assert tree.child.child.op == ">"
assert tree.child.child.value == "0"
# collect_leaves should descend through both NOTs
leaves = collect_leaves(tree)
assert len(leaves) == 1, f"Expected 1 leaf through double NOT, got {len(leaves)}"
assert leaves[0].field == "A"
def test_double_negation_evaluate():
"""CO-DP-06b: evaluate_tree with double negation — cancels out"""
tree = parse_compound_condition("NOT NOT A > 0")
leaves = collect_leaves(tree)
leaf = leaves[0]
# NOT NOT True = True
assert evaluate_tree(tree, {leaf: True}) is True, \
"NOT NOT True should be True"
# NOT NOT False = False
assert evaluate_tree(tree, {leaf: False}) is False, \
"NOT NOT False should be False"
def test_triple_negation():
"""CO-DP-06c: NOT NOT NOT A > 0 — odd negation flips"""
tree = parse_compound_condition("NOT NOT NOT A > 0")
assert tree is not None
leaves = collect_leaves(tree)
leaf = leaves[0]
# NOT (NOT (NOT True)) = NOT (NOT False) = NOT True = False
assert evaluate_tree(tree, {leaf: True}) is False, \
"NOT NOT NOT True should be False"
# NOT (NOT (NOT False)) = NOT (NOT True) = NOT False = True
assert evaluate_tree(tree, {leaf: False}) is True, \
"NOT NOT NOT False should be True"
# ══════════════════════════════════════════════════════════════════
# CO-DP-07: Mixed 3-level NOT/AND/OR evaluation
# ══════════════════════════════════════════════════════════════════
def test_evaluate_mixed_not_and_or_3level():
"""CO-DP-07: NOT (A > 0 AND B < 5) OR (C = 1 AND D <> 2) — mixed 3-level"""
text = "NOT (A > 0 AND B < 5) OR (C = 1 AND D <> 2)"
tree = parse_compound_condition(text)
assert tree is not None
# Root should be CondOr
assert isinstance(tree, CondOr), f"Root expected CondOr, got {type(tree).__name__}"
# Left: NOT (A AND B) → CondNot(CondAnd(A, B))
assert isinstance(tree.left, CondNot), \
f"Left child expected CondNot, got {type(tree.left).__name__}"
not_child = tree.left.child
assert isinstance(not_child, CondAnd), \
f"NOT child expected CondAnd, got {type(not_child).__name__}"
assert isinstance(not_child.left, CondLeaf)
assert not_child.left.field == "A"
assert isinstance(not_child.right, CondLeaf)
assert not_child.right.field == "B"
# Right: (C = 1 AND D <> 2) → CondAnd(C, D)
assert isinstance(tree.right, CondAnd), \
f"Right child expected CondAnd, got {type(tree.right).__name__}"
assert isinstance(tree.right.left, CondLeaf)
assert tree.right.left.field == "C"
assert tree.right.left.op == "="
assert tree.right.left.value == "1"
assert isinstance(tree.right.right, CondLeaf)
assert tree.right.right.field == "D"
assert tree.right.right.op == "<>"
assert tree.right.right.value == "2"
leaves = collect_leaves(tree)
leaf_map = {l.field: l for l in leaves}
assert len(leaf_map) == 4
a = leaf_map["A"]
b = leaf_map["B"]
c = leaf_map["C"]
d = leaf_map["D"]
# NOT (T AND T) OR (F AND T) = NOT T OR F = F OR F = F
assert evaluate_tree(tree, {a: True, b: True, c: False, d: True}) is False
# NOT (F AND T) OR (F AND T) = NOT F OR F = T OR F = T
assert evaluate_tree(tree, {a: False, b: True, c: False, d: True}) is True
# NOT (T AND F) OR (F AND T) = NOT F OR F = T OR F = T
assert evaluate_tree(tree, {a: True, b: False, c: False, d: True}) is True
# NOT (F AND F) OR (T AND T) = NOT F OR T = T OR T = T
assert evaluate_tree(tree, {a: False, b: False, c: True, d: True}) is True
# NOT (T AND T) OR (T AND T) = NOT T OR T = F OR T = T
assert evaluate_tree(tree, {a: True, b: True, c: True, d: True}) is True
# NOT (F AND T) OR (F AND F) = NOT F OR F = T OR F = T
assert evaluate_tree(tree, {a: False, b: True, c: False, d: False}) is True
# NOT (T AND T) OR (T AND F) = NOT T OR F = F OR F = F
assert evaluate_tree(tree, {a: True, b: True, c: True, d: False}) is False
# ══════════════════════════════════════════════════════════════════
# CO-DP-08: 3-input AND MC/DC — should find 4 sets
# ══════════════════════════════════════════════════════════════════
def test_mcdc_3input_and():
"""CO-DP-08: 3-input AND (A>0 AND B<5 AND C=1) → exactly 4 MC/DC sets"""
a = CondLeaf("A", ">", "0")
b = CondLeaf("B", "<", "5")
c = CondLeaf("C", "=", "1")
# Left-deep AND tree: ((A AND B) AND C)
tree = CondAnd(CondAnd(a, b), c)
sets = mcdc_sets(tree)
assert sets is not None, "mcdc_sets should not return None for 3-input AND"
assert len(sets) == 4, f"Expected 4 MC/DC sets for 3-input AND, got {len(sets)}"
# Build constraints lookup
# sets: list of (constraints_list, decision_outcome)
outcomes = {}
for constraints, decision in sets:
# constraint: (field, op, value, want_true)
key = tuple(
(c[0], c[3]) for c in sorted(constraints, key=lambda x: x[0])
)
outcomes[key] = decision
# The 4 required sets covering MC/DC for AND:
# 1. All True → decision True
all_true_key = (("A", True), ("B", True), ("C", True))
assert all_true_key in outcomes, \
f"Missing 'all true' set. Available keys: {list(outcomes.keys())}"
assert outcomes[all_true_key] is True, \
"All-true case should have decision=True"
# 2. A=False, B=True, C=True → shows A's independent effect → decision False
# (Only A flips relative to all-true)
a_effect_key = (("A", False), ("B", True), ("C", True))
assert a_effect_key in outcomes, \
"Missing A-independent-effect set (A=F, B=T, C=T)"
assert outcomes[a_effect_key] is False, \
"A=F should make AND False"
# 3. A=True, B=False, C=True → shows B's independent effect → decision False
b_effect_key = (("A", True), ("B", False), ("C", True))
assert b_effect_key in outcomes, \
"Missing B-independent-effect set (A=T, B=F, C=T)"
assert outcomes[b_effect_key] is False, \
"B=F should make AND False"
# 4. A=True, B=True, C=False → shows C's independent effect → decision False
c_effect_key = (("A", True), ("B", True), ("C", False))
assert c_effect_key in outcomes, \
"Missing C-independent-effect set (A=T, B=T, C=F)"
assert outcomes[c_effect_key] is False, \
"C=F should make AND False"
def test_mcdc_3input_and_parse():
"""CO-DP-08b: 3-input AND from parse_compound_condition → 4 sets"""
tree = parse_compound_condition("A > 0 AND B < 5 AND C = 1")
assert tree is not None
leaves = collect_leaves(tree)
assert len(leaves) == 3, f"Expected 3 leaves, got {len(leaves)}"
sets = mcdc_sets(tree)
assert sets is not None
assert len(sets) == 4, f"Expected 4 MC/DC sets from parsed 3-AND, got {len(sets)}"
# Verify all 3 leaves have independent effect shown
fields_with_false = set()
for constraints, decision in sets:
if decision is False:
false_fields = {c[0] for c in constraints if c[3] is False}
fields_with_false.update(false_fields)
assert "A" in fields_with_false, "A's independent effect not shown"
assert "B" in fields_with_false, "B's independent effect not shown"
assert "C" in fields_with_false, "C's independent effect not shown"
# ══════════════════════════════════════════════════════════════════
# CO-DP-09: 3-input OR MC/DC
# ══════════════════════════════════════════════════════════════════
def test_mcdc_3input_or():
"""CO-DP-09: 3-input OR (A=1 OR B=2 OR C=3) → exactly 4 MC/DC sets"""
a = CondLeaf("A", "=", "1")
b = CondLeaf("B", "=", "2")
c = CondLeaf("C", "=", "3")
tree = CondOr(CondOr(a, b), c)
sets = mcdc_sets(tree)
assert sets is not None
assert len(sets) == 4, f"Expected 4 MC/DC sets for 3-input OR, got {len(sets)}"
outcomes = {}
for constraints, decision in sets:
key = tuple(
(c[0], c[3]) for c in sorted(constraints, key=lambda x: x[0])
)
outcomes[key] = decision
# 1. All False → decision False
all_false_key = (("A", False), ("B", False), ("C", False))
assert all_false_key in outcomes, "Missing 'all false' set for OR"
assert outcomes[all_false_key] is False
# 2. A=True, B=False, C=False → A's independent effect
a_key = (("A", True), ("B", False), ("C", False))
assert a_key in outcomes, "Missing A-independent-effect set for OR"
assert outcomes[a_key] is True
# 3. A=False, B=True, C=False → B's independent effect
b_key = (("A", False), ("B", True), ("C", False))
assert b_key in outcomes, "Missing B-independent-effect set for OR"
assert outcomes[b_key] is True
# 4. A=False, B=False, C=True → C's independent effect
c_key = (("A", False), ("B", False), ("C", True))
assert c_key in outcomes, "Missing C-independent-effect set for OR"
assert outcomes[c_key] is True
# ══════════════════════════════════════════════════════════════════
# CO-DP-10: Edge cases — boundary and unusual inputs
# ══════════════════════════════════════════════════════════════════
def test_compound_no_fields_arg():
"""CO-DP-10a: parse_compound_condition without fields arg still works"""
tree = parse_compound_condition("A > 0 AND B < 5")
assert tree is not None
assert isinstance(tree, CondAnd)
def test_deep_chain_of_and():
"""CO-DP-10b: 10-input AND chain — all leaves collected correctly"""
text = " AND ".join(f"V{i} = {i}" for i in range(10))
tree = parse_compound_condition(text)
assert tree is not None
leaves = collect_leaves(tree)
assert len(leaves) == 10, f"Expected 10 leaves, got {len(leaves)}"
values = [(l.field, l.value) for l in leaves]
for i in range(10):
assert (f"V{i}", str(i)) in values, f"V{i} = {i} not found in tree"
def test_nested_parens_deep():
"""CO-DP-10c: Deeply nested parentheses — (((A > 0))) → CondLeaf"""
tree = parse_compound_condition("(((A > 0)))")
assert tree is not None
assert isinstance(tree, CondLeaf)
assert tree.field == "A"
def test_collect_leaves_on_leaf():
"""CO-DP-10d: collect_leaves on a single CondLeaf returns [leaf]"""
leaf = CondLeaf("X", "=", "1")
result = collect_leaves(leaf)
assert len(result) == 1
assert result[0] is leaf
def test_collect_leaves_on_empty_not():
"""CO-DP-10e: CondNot with CondNot leaf still returns leaves"""
leaf = CondLeaf("X", "=", "1")
tree = CondNot(CondNot(leaf))
leaves = collect_leaves(tree)
assert len(leaves) == 1
assert leaves[0] is leaf
def test_satisfying_value_zero_length():
"""CO-DP-10f: satisfying_value with zero digits — fallback to '0'"""
info = {"type": "unknown", "digits": 0, "decimal": 0}
result = satisfying_value(info, "=", "X", want_true=True)
# Falls through to return '0'.zfill(0) = ''
assert result is not None
# ══════════════════════════════════════════════════════════════════
# CO-DP-11: Compound with NOT wrapping sub-expressions
# ══════════════════════════════════════════════════════════════════
def test_not_wrapping_and():
"""CO-DP-11: NOT (A > 0 AND B < 5) — NOT wrapping AND"""
tree = parse_compound_condition("NOT (A > 0 AND B < 5)")
assert tree is not None
assert isinstance(tree, CondNot)
assert isinstance(tree.child, CondAnd)
leaves = collect_leaves(tree)
assert len(leaves) == 2
leaf = leaves[0] # A
# NOT (T AND T) = NOT T = F
assert evaluate_tree(tree, {leaf: True, leaves[1]: True}) is False
# NOT (F AND T) = NOT F = T
assert evaluate_tree(tree, {leaf: False, leaves[1]: True}) is True
def test_not_wrapping_or():
"""CO-DP-11b: NOT (A = 1 OR B = 2) — NOT wrapping OR"""
tree = parse_compound_condition("NOT (A = 1 OR B = 2)")
assert tree is not None
assert isinstance(tree, CondNot)
assert isinstance(tree.child, CondOr)
leaves = collect_leaves(tree)
assert len(leaves) == 2
assert evaluate_tree(tree, {leaves[0]: False, leaves[1]: False}) is True
assert evaluate_tree(tree, {leaves[0]: True, leaves[1]: False}) is False
# ══════════════════════════════════════════════════════════════════
# CO-DP-12: mcdc_sets edge cases
# ══════════════════════════════════════════════════════════════════
def test_mcdc_single_not_leaf():
"""CO-DP-12a: mcdc_sets on single NOT leaf returns None (only 1 leaf)"""
tree = CondNot(CondLeaf("A", ">", "0"))
# collect_leaves gives 1 leaf through the NOT
result = mcdc_sets(tree)
assert result is None, "Single leaf (even through NOT) should return None"
def test_mcdc_and_not_mix():
"""CO-DP-12b: mcdc_sets on (A=1 AND NOT B=2) — mixed AND/NOT"""
tree = CondAnd(
CondLeaf("A", "=", "1"),
CondNot(CondLeaf("B", "=", "2")),
)
sets = mcdc_sets(tree)
assert sets is not None
assert len(sets) >= 3, f"Expected >= 3 sets, got {len(sets)}"
# Verify B's independent effect
all_fields = set()
for constraints, decision in sets:
for c in constraints:
all_fields.add(c[0])
assert "A" in all_fields
assert "B" in all_fields
def test_mcdc_evaluate_consistency():
"""CO-DP-12c: All MC/DC constraints, when evaluated, produce the decision they claim"""
a = CondLeaf("A", ">", "0")
b = CondLeaf("B", "<", "5")
c = CondLeaf("C", "=", "1")
tree = CondAnd(CondAnd(a, b), c)
leaves = [a, b, c]
sets = mcdc_sets(tree)
assert sets is not None
for constraints, expected_decision in sets:
# Build assignment from constraints: (field, op, value, want_true)
assignment = {}
for constr in constraints:
field, op, value, want = constr
# Find matching leaf by field
for leaf in leaves:
if leaf.field == field:
assignment[leaf] = want
break
# Verify this assignment produces the claimed decision
actual = evaluate_tree(tree, assignment)
assert actual == expected_decision, (
f"MC/DC set inconsistency: expected decision={expected_decision}, "
f"but evaluate_tree returned {actual} for constraints={constraints}"
)
# ══════════════════════════════════════════════════════════════════
# CO-DP-13: NOT with <>, numeric edge cases in satisfying_value
# ══════════════════════════════════════════════════════════════════
def test_satisfying_value_not_via_want_false():
"""CO-DP-13: '= ... want_true=False' simulates COBOL 'NOT ='"""
info = {"type": "numeric", "digits": 5, "decimal": 0}
# The condition `NOT WS-FIELD = 100` is equivalent to `WS-FIELD <> 100`
# = want_true=False means we want value != target
eq_f = satisfying_value(info, "=", "100", want_true=False)
assert int(eq_f) != 100
# <> want_true=True also means we want value != target
ne = satisfying_value(info, "<>", "100", want_true=True)
assert int(ne) != 100
# They should both produce values != 100 (not necessarily the same value)
assert int(eq_f) != 100
assert int(ne) != 100
def test_mcdc_not_in_compound_all_outcomes():
"""CO-DP-13b: Verify MC/DC covers both True/False branches for NOT leaf"""
# (A = 1 AND NOT B = 2) — a simple 2-leaf case with a NOT
tree = parse_compound_condition("A = 1 AND NOT B = 2")
assert tree is not None
sets = mcdc_sets(tree)
assert sets is not None
decisions = set(d for _, d in sets)
assert True in decisions, "Should have a True decision branch"
assert False in decisions, "Should have a False decision branch"