"""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"