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:
@@ -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"
|
||||
Reference in New Issue
Block a user