bc1d56d1a4
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>
434 lines
14 KiB
Python
434 lines
14 KiB
Python
"""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
|