Files
cobol-java-v3/tests/cobol_testgen/test_cond_deep.py
hangshuo652 bc1d56d1a4 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>
2026-06-19 23:51:55 +08:00

844 lines
36 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"