"""Deep coverage tests: HTML report, SEARCH/EVALUATE/PERFORM coverage, locate, index""" import sys, os sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) from cobol_testgen.models import BrSeq, CondLeaf from cobol_testgen.coverage import ( DecisionPoint, LeafStat, mark_coverage, generate_html_report, generate_coverage_index, locate_decision_lines, check_coverage, ) # ── 1. generate_html_report ── def test_generate_html_report_full(tmp_path): """Generate full HTML report with known DecisionPoint data — assert table, branch rate, decision points""" dps = [ DecisionPoint(id=1, kind="IF", label="A > 100", branch_names=["T", "F"], active_branches={"T"}, implied_branches={"T"}, source_line=4), DecisionPoint(id=2, kind="EVALUATE", label="WS-STATUS", branch_names=["WHEN 1", "WHEN 2", "OTHER"], active_branches={"WHEN 1"}, implied_branches={"WHEN 1"}, source_line=7), ] leaves = [ LeafStat(field="A", op=">", value="100", covered_true=True, covered_false=False), LeafStat(field="B", op="=", value="1", covered_true=False, covered_false=False), ] source_lines = [ " IDENTIFICATION DIVISION.", " PROGRAM-ID. TESTPGM.", " PROCEDURE DIVISION.", " IF A > 100", " MOVE 1 TO B", " END-IF.", " EVALUATE WS-STATUS", " WHEN 1 ...", " END-EVALUATE.", " STOP RUN.", ] outpath = tmp_path / "TESTPGM_coverage.html" generate_html_report(dps, leaves, source_lines, outpath, filename="TESTPGM") html = outpath.read_text(encoding="utf-8") # HTML structure assert " for decision point list" assert "覆盖率报告" in html, "Should contain report title" assert "TESTPGM" in html, "Should contain program name in title" # Branch rate percentage # total=5, covered=2 → 40.0% assert "40.0%" in html or "2/5" in html # Coverage section texts assert "决策覆盖率" in html assert "条件覆盖率" in html # Decision point list items assert "#1" in html assert "#2" in html assert "IF" in html assert "EVALUATE" in html assert "branch-true" in html assert "branch-false" in html # Leaf stats table assert "A" in html assert "B" in html # Source lines assert "IF A > 100" in html assert "EVALUATE WS-STATUS" in html assert "hl-green" in html # IF line is fully covered def test_generate_html_report_no_decision_points(tmp_path): """No decision points → no branch table, no SVG""" outpath = tmp_path / "empty_report.html" generate_html_report([], [], [], outpath, filename="EMPTYPGM") html = outpath.read_text(encoding="utf-8") assert "EMPTYPGM" in html # No DP table rows (0个决策点 shown as stat) assert "0个" in html or "0%" in html # Still has the summary section assert "覆盖率概要" in html # ── 2. BrSearch (SEARCH ALL) coverage via _mark_search ── def test_mark_search_covered_first_branch(): """SEARCH ALL DecisionPoint with CondLeaf when_list — first WHEN branch covered""" dp = DecisionPoint(id=1, kind="SEARCH", label="WS-TABLE", branch_names=["WHEN A > 100", "WHEN B = 50", "AT END"]) dp.when_list = [ ("A > 100", BrSeq()), ("B = 50", BrSeq()), ] dp.cond_trees = [ CondLeaf("A", ">", "100"), CondLeaf("B", "=", "50"), ] dp.has_other = True leaf_stats = [] branch_paths = [ ([("A", ">", "100", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "WHEN A > 100" in dp.active_branches assert "AT END" not in dp.active_branches assert "WHEN B = 50" not in dp.active_branches def test_mark_search_covered_at_end(): """SEARCH ALL — no WHEN matches → AT END covered""" dp = DecisionPoint(id=1, kind="SEARCH", label="WS-TABLE", branch_names=["WHEN K > 10", "AT END"]) dp.when_list = [ ("K > 10", BrSeq()), ] dp.cond_trees = [ CondLeaf("K", ">", "10"), ] dp.has_other = True leaf_stats = [] # K <= 10 → no WHEN matches branch_paths = [ ([("K", ">", "10", False)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "AT END" in dp.active_branches assert "WHEN K > 10" not in dp.active_branches def test_mark_search_compound_condition(): """SEARCH ALL with compound condition tree""" dp = DecisionPoint(id=1, kind="SEARCH", label="WS-TABLE", branch_names=["WHEN A>1 AND B<9", "AT END"]) dp.when_list = [ ("A > 1 AND B < 9", BrSeq()), ] # Build compound tree: CondAnd(CondLeaf("A", ">", "1"), CondLeaf("B", "<", "9")) dp.cond_trees = [ type('obj', (object,), { 'field': 'dummy', 'op': '=', 'value': '0', '__class__': CondLeaf.__class__, }) # won't be used — tree is CondAnd type ] # Actually use a proper tree from cobol_testgen.models import CondAnd dp.cond_trees = [ CondAnd(CondLeaf("A", ">", "1"), CondLeaf("B", "<", "9")) ] dp.has_other = True leaf_stats = [] branch_paths = [ ([("A", ">", "1", True), ("B", "<", "9", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "WHEN A>1 AND B<9" in dp.active_branches assert "AT END" not in dp.active_branches # ── 3. BrEval with multiple subjects (ALSO) — _mark_eval ── def test_mark_eval_simple(): """EVALUATE with subject match via constraint field=subject""" dp = DecisionPoint(id=1, kind="EVALUATE", label="WS-STATUS", branch_names=["WHEN 1", "WHEN 2", "OTHER"]) dp.when_list = [ ("1", BrSeq()), ("2", BrSeq()), ] leaf_stats = [] branch_paths = [ ([("WS-STATUS", "=", "1", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "WHEN 1" in dp.active_branches assert "WHEN 2" not in dp.active_branches assert "OTHER" not in dp.active_branches def test_mark_eval_other_branch(): """EVALUATE — not_in constraint triggers OTHER""" dp = DecisionPoint(id=1, kind="EVALUATE", label="WS-STATUS", branch_names=["WHEN 1", "WHEN 2", "OTHER"]) dp.when_list = [ ("1", BrSeq()), ("2", BrSeq()), ] leaf_stats = [] branch_paths = [ ([("WS-STATUS", "not_in", "", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "OTHER" in dp.active_branches def test_mark_eval_true_subject(): """EVALUATE TRUE with matched WHEN branch""" dp = DecisionPoint(id=1, kind="EVALUATE", label="TRUE", branch_names=["WHEN A > 100", "WHEN B = 0", "OTHER"]) dp.when_list = [ ("A > 100", BrSeq()), ("B = 0", BrSeq()), ] leaf_stats = [] branch_paths = [ ([("A", ">", "100", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "WHEN A > 100" in dp.active_branches # ── 4. BrPerform UNTIL — _mark_perform ── def test_mark_perform_until_skip(): """PERFORM UNTIL condition true → Skip branch active""" dp = DecisionPoint(id=1, kind="PERFORM", label="A > 100", branch_names=["Enter", "Skip"]) # Simulate the "parsed" attribute set by collect_decision_points dp.parsed = ("A", ">", "100") leaf_stats = [] branch_paths = [ ([("A", ">", "100", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "Skip" in dp.active_branches assert "Enter" not in dp.active_branches def test_mark_perform_until_enter(): """PERFORM UNTIL condition false → Enter branch active""" dp = DecisionPoint(id=1, kind="PERFORM", label="A > 100", branch_names=["Enter", "Skip"]) dp.parsed = ("A", ">", "100") leaf_stats = [] branch_paths = [ ([("A", ">", "100", False)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "Enter" in dp.active_branches assert "Skip" not in dp.active_branches def test_mark_perform_until_compound(): """PERFORM UNTIL with compound condition tree""" from cobol_testgen.models import CondAnd leaf_a = CondLeaf("A", ">", "100") leaf_b = CondLeaf("B", "<", "50") dp = DecisionPoint(id=1, kind="PERFORM", label="A > 100 AND B < 50", branch_names=["Enter", "Skip"]) dp.cond_tree = CondAnd(leaf_a, leaf_b) dp.cond_leaves = [leaf_a, leaf_b] leaf_stats = [] branch_paths = [ ([("A", ">", "100", True), ("B", "<", "50", True)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "Skip" in dp.active_branches def test_mark_perform_until_compound_false(): """PERFORM UNTIL compound false → Enter active""" from cobol_testgen.models import CondAnd leaf_a = CondLeaf("A", ">", "100") leaf_b = CondLeaf("B", "<", "50") dp = DecisionPoint(id=1, kind="PERFORM", label="A > 100 AND B < 50", branch_names=["Enter", "Skip"]) dp.cond_tree = CondAnd(leaf_a, leaf_b) dp.cond_leaves = [leaf_a, leaf_b] leaf_stats = [] branch_paths = [ ([("A", ">", "100", True), ("B", "<", "50", False)], []), ] mark_coverage([dp], leaf_stats, branch_paths, []) assert "Enter" in dp.active_branches # ── 5. locate_decision_lines with real COBOL ── def test_locate_decision_lines_complex(): """Mixed IF/EVALUATE/SEARCH ALL COBOL source → correct line numbers""" source = """ IDENTIFICATION DIVISION. PROGRAM-ID. TESTPGM. DATA DIVISION. WORKING-STORAGE SECTION. 01 WS-A PIC 9(4). PROCEDURE DIVISION. IF WS-A > 100 MOVE 1 TO B END-IF. EVALUATE WS-A WHEN 1 MOVE 'A' TO B WHEN 2 MOVE 'B' TO B WHEN OTHER MOVE 'C' TO B END-EVALUATE. SEARCH ALL WS-TABLE AT END DISPLAY 'NOT FOUND' WHEN WS-KEY = 1 DISPLAY 'FOUND' END-SEARCH. STOP RUN. END PROGRAM TESTPGM.""" dps = [ DecisionPoint(id=1, kind="IF", label="WS-A > 100", branch_names=["T", "F"]), DecisionPoint(id=2, kind="EVALUATE", label="WS-A", branch_names=["WHEN 1", "WHEN 2", "OTHER"]), # SEARCH kind is not located by _build_search_patterns, expect 0 DecisionPoint(id=3, kind="SEARCH", label="WS-TABLE", branch_names=["WHEN K=1", "AT END"]), ] locate_decision_lines(dps, source) assert dps[0].source_line == 7 # IF WS-A > 100 assert dps[1].source_line == 10 # EVALUATE WS-A assert dps[2].source_line == 0 # SEARCH not located (no pattern) # ── 6. check_coverage with real-style structure ── def test_check_coverage_with_structure(): """Real-style structure dict with decision_points list and records""" structure = { "total_paragraphs": 5, "total_branches": 10, "decision_points": [ {"kind": "IF", "branch_names": ["T", "F"]}, {"kind": "EVALUATE", "branch_names": ["W1", "W2", "OTHER"]}, ], } test_records = [{"id": 1, "case": "CASE01"}, {"id": 2, "case": "CASE02"}] result = check_coverage(structure, test_records) assert isinstance(result, dict) assert result["paragraph_rate"] == 1.0 # has records + paragraphs > 0 assert result["branch_rate"] == 0.0 # static analysis limitation assert result["decision_rate"] == 0.0 assert result["total_branches"] == 10 assert result["total_paragraphs"] == 5 assert result["records_count"] == 2 assert "gcov" in result["note"] def test_check_coverage_no_records(): """No test records → paragraph_rate = 0.0""" structure = {"total_paragraphs": 3, "total_branches": 5, "decision_points": []} result = check_coverage(structure, []) assert result["paragraph_rate"] == 0.0 assert result["records_count"] == 0 def test_check_coverage_no_paragraphs(): """No paragraphs but records exist → paragraph_rate = 0.0""" structure = {"total_paragraphs": 0, "total_branches": 5, "decision_points": []} result = check_coverage(structure, [{"id": 1}]) assert result["paragraph_rate"] == 0.0 # ── 7. generate_coverage_index with 2 programs ── def test_generate_coverage_index_two_programs(tmp_path): """Index page with 2 programs → HTML contains both names and SVG ring charts""" programs = [ { "name": "PGM001", "detail_relpath": "../PGM001_coverage.html", "total_branches": 5, "covered_branches": 4, "implied_branches": 4, "total_conditions": 6, "covered_conditions": 5, }, { "name": "PGM002", "detail_relpath": "../PGM002_coverage.html", "total_branches": 3, "covered_branches": 3, "implied_branches": 3, "total_conditions": 4, "covered_conditions": 4, }, ] generate_coverage_index(programs, str(tmp_path)) index_path = tmp_path / "coverage" / "index.html" assert index_path.exists() html = index_path.read_text(encoding="utf-8") # Both program names assert "PGM001" in html assert "PGM002" in html # Links to detail pages assert "PGM001_coverage.html" in html assert "PGM002_coverage.html" in html # SVG ring chart assert "