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,433 @@
|
||||
"""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 "<table" in html, "Should contain <table> 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 "<svg" in html
|
||||
assert "circle" in html
|
||||
assert "100%" in html or "80.0%" # PGM002 is 100%, PGM001 is 80%
|
||||
|
||||
# Coverage text
|
||||
assert "覆盖率总览" in html
|
||||
assert "决策覆盖率" in html
|
||||
assert "条件覆盖率" in html
|
||||
Reference in New Issue
Block a user