466 lines
16 KiB
Python
466 lines
16 KiB
Python
"""
|
|
cobol-java-v3 平台用户故事测试
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
测试对象: cobol-java-v3 平台自身(不是COBOL程序)
|
|
测试范围: 正常 / 异常 / 边界 / 缺陷 4类用户故事
|
|
|
|
执行: python -X utf8 test-data/test_platform_user_stories.py
|
|
"""
|
|
import sys, os, json, time, tempfile, shutil, traceback
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from data.diff_result import VerificationRun, FieldResult
|
|
from data.test_case import TestCase, TestSuite, SparkConfig
|
|
from data.field_tree import FieldTree
|
|
|
|
PASS = 0
|
|
FAIL = 0
|
|
ERRORS = []
|
|
|
|
def section(title):
|
|
print(f"\n{'─'*70}")
|
|
print(f" {title}")
|
|
print(f"{'─'*70}")
|
|
|
|
def test(name, category):
|
|
def decorator(fn):
|
|
global PASS, FAIL
|
|
try:
|
|
fn()
|
|
PASS += 1
|
|
print(f" [{category}] {name} → ✅ PASS")
|
|
except Exception as e:
|
|
FAIL += 1
|
|
tb = traceback.format_exc()[-300:]
|
|
ERRORS.append(f"{name}: {e}")
|
|
print(f" [{category}] {name} → ❌ FAIL: {e}")
|
|
print(f" {tb.split(chr(10))[-3]}")
|
|
return fn
|
|
return decorator
|
|
|
|
# ════════════════════════════════════════════
|
|
# 正常系 — Normal
|
|
# ════════════════════════════════════════════
|
|
|
|
section("N: 正常系ユーザーストーリー")
|
|
|
|
@test("VerificationRun 作成と全フィールド設定", "NORMAL")
|
|
def _():
|
|
vr = VerificationRun(program="TESTPGM", runner="native")
|
|
assert vr.program == "TESTPGM"
|
|
assert vr.runner == "native"
|
|
assert vr.timestamp != ""
|
|
vr.branch_rate = 0.95
|
|
vr.paragraph_rate = 1.0
|
|
vr.hina_type = "マッチング"
|
|
vr.quality_score = 0.85
|
|
vr.heal_retry = 1
|
|
assert vr.branch_rate == 0.95
|
|
assert vr.hina_type == "マッチング"
|
|
|
|
@test("TestCase 作成とフィールド設定", "NORMAL")
|
|
def _():
|
|
tc = TestCase(id="TC-001", fields={"BR-AMT": 1500, "BR-STATUS": "A"})
|
|
assert tc.id == "TC-001"
|
|
assert tc.fields["BR-AMT"] == 1500
|
|
assert tc.fields["BR-STATUS"] == "A"
|
|
assert tc.coverage_targets == []
|
|
|
|
@test("FieldResult 作成とステータス", "NORMAL")
|
|
def _():
|
|
fr = FieldResult(field_name="BR-AMT", status="PASS", cobol_value="1500", java_value="1500.00")
|
|
assert fr.field_name == "BR-AMT"
|
|
assert fr.status == "PASS"
|
|
fr.status = "MISMATCH"
|
|
assert fr.status == "MISMATCH"
|
|
|
|
@test("Config デフォルト値", "NORMAL")
|
|
def _():
|
|
from config import Config
|
|
c = Config()
|
|
assert c.quality_gate_mode == "warn"
|
|
assert c.runner_mode == "native"
|
|
assert c.dialect == "ibm"
|
|
assert c.gcov_enabled == False
|
|
assert c.max_quality_retries == 4
|
|
|
|
@test("Config from_toml 正常", "NORMAL")
|
|
def _():
|
|
from config import Config
|
|
c = Config.from_toml(path=Path(__file__).parent.parent / "aurak.toml")
|
|
assert c.project_name != "" or c.runner_mode != ""
|
|
|
|
@test("VerificationRun total_fields 計算", "NORMAL")
|
|
def _():
|
|
vr = VerificationRun(fields_matched=10, fields_mismatched=2)
|
|
assert vr.total_fields == 12
|
|
|
|
@test("HINA classifier L1: DB操作", "NORMAL")
|
|
def _():
|
|
from hina.classifier import detect_keyword
|
|
r = detect_keyword("EXEC SQL SELECT * FROM TABLE END-EXEC")
|
|
assert any("DB操作" in x[0] for x in r)
|
|
assert any(x[1] >= 0.95 for x in r)
|
|
|
|
@test("HINA classifier L1: CALL", "NORMAL")
|
|
def _():
|
|
from hina.classifier import detect_keyword
|
|
r = detect_keyword("CALL 'SUBPGM' USING A.\nLINKAGE SECTION.")
|
|
assert any("子程序调用" in x[0] for x in r)
|
|
|
|
@test("HINA strategy マッチングテンプレート", "NORMAL")
|
|
def _():
|
|
from hina.strategy import get_strategy
|
|
s = get_strategy("マッチング")
|
|
assert len(s["required"]) == 9
|
|
|
|
@test("Quality gate: 合格", "NORMAL")
|
|
def _():
|
|
from hina.gate import check
|
|
r = check([{"a": 1}], {}, {"branch_rate": 0.95, "paragraph_rate": 1.0, "uncovered_decision_ids": []})
|
|
assert r["passed"] == True
|
|
|
|
@test("RetryHandler: 即PASS", "NORMAL")
|
|
def _():
|
|
from hina.retry import RetryHandler
|
|
h = RetryHandler()
|
|
vr = h.run(lambda: VerificationRun(status="PASS"))
|
|
assert vr.status == "PASS"
|
|
assert vr.heal_retry == 0
|
|
|
|
@test("ReportGenerator: HTML生成", "NORMAL")
|
|
def _():
|
|
from report.generator import ReportGenerator
|
|
vr = VerificationRun(program="TEST", runner="native")
|
|
rd = Path(tempfile.mkdtemp())
|
|
try:
|
|
g = ReportGenerator()
|
|
p = g.generate_html(vr, rd / "test.html")
|
|
assert p.exists()
|
|
html = p.read_text(encoding="utf-8")
|
|
assert "TEST" in html
|
|
finally:
|
|
shutil.rmtree(rd)
|
|
|
|
@test("ReportGenerator: HTML カバレッジ表示", "NORMAL")
|
|
def _():
|
|
from report.generator import ReportGenerator
|
|
vr = VerificationRun(program="T1", paragraph_rate=0.9, branch_rate=0.85)
|
|
rd = Path(tempfile.mkdtemp())
|
|
try:
|
|
p = ReportGenerator().generate_html(vr, rd / "t.html")
|
|
html = p.read_text(encoding="utf-8")
|
|
assert "段落覆盖率" in html
|
|
assert "分支覆盖率" in html
|
|
finally:
|
|
shutil.rmtree(rd)
|
|
|
|
@test("ReportGenerator: HTML HINA表示", "NORMAL")
|
|
def _():
|
|
from report.generator import ReportGenerator
|
|
vr = VerificationRun(program="T2", hina_type="マッチング", hina_confidence=0.95)
|
|
rd = Path(tempfile.mkdtemp())
|
|
try:
|
|
p = ReportGenerator().generate_html(vr, rd / "t.html")
|
|
assert "HINA" in p.read_text(encoding="utf-8")
|
|
finally:
|
|
shutil.rmtree(rd)
|
|
|
|
@test("ReportGenerator: JSON 新フィールド", "NORMAL")
|
|
def _():
|
|
from report.generator import ReportGenerator
|
|
vr = VerificationRun(program="T3", branch_rate=0.9, quality_score=0.85)
|
|
rd = Path(tempfile.mkdtemp())
|
|
try:
|
|
p = ReportGenerator().generate_json(vr, rd / "t.json")
|
|
d = json.loads(p.read_text())
|
|
assert d["branch_rate"] == 0.9
|
|
assert d["quality_score"] == 0.85
|
|
finally:
|
|
shutil.rmtree(rd)
|
|
|
|
@test("cobol_testgen extract_structure: IF", "NORMAL")
|
|
def _():
|
|
from cobol_testgen import extract_structure
|
|
s = extract_structure("PROCEDURE DIVISION.\nIF A>B MOVE 1 TO C ELSE MOVE 2 TO C.\nGOBACK.")
|
|
assert "paragraphs" in s
|
|
assert "decision_points" in s
|
|
|
|
# ════════════════════════════════════════════
|
|
# 異常系 — Abnormal
|
|
# ════════════════════════════════════════════
|
|
|
|
section("A: 異常系ユーザーストーリー")
|
|
|
|
@test("空COBOLソース→extract_structure", "ABNORMAL")
|
|
def _():
|
|
from cobol_testgen import extract_structure
|
|
s = extract_structure("")
|
|
assert s is not None
|
|
assert s.get("total_branches", 0) == 0
|
|
|
|
@test("PROCEDURE DIVISIONなし→extract_structure", "ABNORMAL")
|
|
def _():
|
|
from cobol_testgen import extract_structure
|
|
s = extract_structure("IDENTIFICATION DIVISION.\nPROGRAM-ID. X.\nDATA DIVISION.\nWORKING-STORAGE SECTION.\n01 A PIC X(10).")
|
|
assert s is not None
|
|
assert "paragraphs" in s
|
|
|
|
@test("Quality gate: 空データ", "ABNORMAL")
|
|
def _():
|
|
from hina.gate import check
|
|
r = check([], {}, {"branch_rate": 0.0, "paragraph_rate": 0.0, "uncovered_decision_ids": []})
|
|
assert r["passed"] == False
|
|
assert "no_data" in r.get("issues", {})
|
|
|
|
@test("Quality gate: 分岐不足", "ABNORMAL")
|
|
def _():
|
|
from hina.gate import check
|
|
r = check([{"x": 1}], {}, {"branch_rate": 0.5, "paragraph_rate": 1.0, "uncovered_decision_ids": [1, 2]})
|
|
assert r["passed"] == False
|
|
assert "decision_gaps" in r.get("issues", {})
|
|
|
|
@test("RetryHandler: 全FAIL→FATAL", "ABNORMAL")
|
|
def _():
|
|
from hina.retry import RetryHandler
|
|
from data.diff_result import VerificationRun
|
|
h = RetryHandler(max_heal=1, max_simple=1)
|
|
vr = h.run(lambda: VerificationRun(status="ERROR", exit_code=3))
|
|
assert vr.status == "FATAL"
|
|
assert vr.exit_code == 4
|
|
|
|
@test("Config: 必須fieldなし", "ABNORMAL")
|
|
def _():
|
|
from config import Config
|
|
c = Config.from_toml(path="nonexistent.toml")
|
|
assert c.runner_mode == "native"
|
|
assert c.quality_gate_mode == "warn"
|
|
|
|
@test("extract_structure: 不正COBOL構文", "ABNORMAL")
|
|
def _():
|
|
from cobol_testgen import extract_structure
|
|
s = extract_structure("THIS IS NOT VALID COBOL @@@ @@@")
|
|
assert s is not None
|
|
|
|
@test("generate_data: 分岐なしプログラム", "ABNORMAL")
|
|
def _():
|
|
from cobol_testgen import generate_data
|
|
s = "PROCEDURE DIVISION.\nGOBACK."
|
|
r = generate_data(s)
|
|
assert isinstance(r, list)
|
|
assert len(r) == 0
|
|
|
|
@test("incremental_supplement: 存在しないID", "ABNORMAL")
|
|
def _():
|
|
from cobol_testgen import incremental_supplement
|
|
r = incremental_supplement(None, [-1])
|
|
assert isinstance(r, list)
|
|
|
|
@test("VerificationRun: 空フィールド", "ABNORMAL")
|
|
def _():
|
|
vr = VerificationRun()
|
|
assert vr.total_fields == 0
|
|
assert vr.status == "PASS"
|
|
|
|
@test("HINA classifier: キーワードなし", "ABNORMAL")
|
|
def _():
|
|
from hina.classifier import compute_confidence
|
|
r = compute_confidence("PROCEDURE DIVISION.\nDISPLAY 'HELLO'.")
|
|
assert r["category"] == "unknown"
|
|
assert r["confidence"] == 0.0
|
|
|
|
@test("HINA strategy: 未知のタイプ", "ABNORMAL")
|
|
def _():
|
|
from hina.strategy import get_strategy
|
|
s = get_strategy("UNKNOWN_TYPE_XXX")
|
|
assert s["required"] == []
|
|
|
|
@test("gcov_collector: ファイルなし", "ABNORMAL")
|
|
def _():
|
|
from hina.gcov_collector import collect_gcov
|
|
r = collect_gcov(Path("nonexistent.cbl"), Path("/dev/null"))
|
|
assert r["available"] == False
|
|
assert "reason" in r
|
|
|
|
# ════════════════════════════════════════════
|
|
# 境界系 — Boundary
|
|
# ════════════════════════════════════════════
|
|
|
|
section("B: 境界系ユーザーストーリー")
|
|
|
|
@test("超巨大プログラム: 1000個IF", "BOUNDARY")
|
|
def _():
|
|
from cobol_testgen import extract_structure
|
|
lines = ["PROCEDURE DIVISION."]
|
|
for i in range(1000):
|
|
lines.append(f"IF A > {i} THEN MOVE {i} TO X ELSE MOVE {i} TO Y END-IF.")
|
|
lines.append("GOBACK.")
|
|
src = "\n".join(lines)
|
|
t0 = time.time()
|
|
s = extract_structure(src)
|
|
elapsed = time.time() - t0
|
|
print(f" → 1000 IF: {elapsed:.1f}s, 安定")
|
|
assert s is not None
|
|
assert elapsed < 10 # 10秒以内に完了
|
|
|
|
@test("超長フィールド名: 1000文字", "BOUNDARY")
|
|
def _():
|
|
from cobol_testgen import extract_structure
|
|
long = "A" * 1000
|
|
src = f"""IDENTIFICATION DIVISION.
|
|
PROGRAM-ID. X.
|
|
DATA DIVISION.
|
|
WORKING-STORAGE SECTION.
|
|
01 {long} PIC X(10).
|
|
PROCEDURE DIVISION.
|
|
GOBACK."""
|
|
s = extract_structure(src)
|
|
assert s is not None
|
|
|
|
@test("TestSuite 0件", "BOUNDARY")
|
|
def _():
|
|
ts = TestSuite()
|
|
assert ts.has_spark == False
|
|
assert len(ts.test_cases) == 0
|
|
|
|
@test("SparkConfig 大量レコード", "BOUNDARY")
|
|
def _():
|
|
from data.test_case import SparkConfig
|
|
sc = SparkConfig(num_records=100000)
|
|
assert sc.num_records == 100000
|
|
|
|
@test("VerificationRun 全フィールド最大値", "BOUNDARY")
|
|
def _():
|
|
vr = VerificationRun(fields_matched=9999, fields_mismatched=9999)
|
|
assert vr.total_fields == 19998
|
|
vr.branch_rate = 1.0
|
|
vr.quality_score = 1.0
|
|
assert vr.branch_rate == 1.0
|
|
|
|
@test("100並列TestCases作成", "BOUNDARY")
|
|
def _():
|
|
cases = [TestCase(id=f"TC-{i:04d}", fields={"X": i}) for i in range(100)]
|
|
assert len(cases) == 100
|
|
assert cases[0].id == "TC-0000"
|
|
assert cases[99].id == "TC-0099"
|
|
|
|
# ════════════════════════════════════════════
|
|
# 欠陥系 — Defect (過去修正したバグの回帰)
|
|
# ════════════════════════════════════════════
|
|
|
|
section("D: 欠陥系ユーザーストーリー (回帰テスト)")
|
|
|
|
@test("DEFECT-001:complete_tests→DataWriter", "DEFECT")
|
|
def _():
|
|
"""P1修复: complete_tests 必须传递给 DataWriter"""
|
|
from data.test_case import TestCase
|
|
tc = TestCase(id="CTG-0001", fields={"TX-AMT": 100})
|
|
assert tc.id == "CTG-0001"
|
|
assert tc.fields["TX-AMT"] == 100
|
|
# DataWriter 接受 TestCase[]
|
|
from data.test_case import TestSuite
|
|
ts = TestSuite(test_cases=[tc])
|
|
assert len(ts.test_cases) == 1
|
|
|
|
@test("DEFECT-002:质量门禁循环中同步更新", "DEFECT")
|
|
def _():
|
|
"""P2修复: 增量补充后complete_tests需要更新"""
|
|
from data.test_case import TestCase
|
|
base = [TestCase(id=f"B{i}", fields={"v": i}) for i in range(3)]
|
|
delta = [TestCase(id=f"D{i}", fields={"v": i+10}) for i in range(2)]
|
|
combined = base + delta
|
|
assert len(combined) == 5
|
|
assert combined[3].id == "D0"
|
|
|
|
@test("DEFECT-003:分层重试 heal恢复", "DEFECT")
|
|
def _():
|
|
"""分层重试: heal修复后应成功"""
|
|
from hina.retry import RetryHandler
|
|
from data.diff_result import VerificationRun
|
|
called = [0]
|
|
def fn():
|
|
called[0] += 1
|
|
if called[0] <= 2:
|
|
return VerificationRun(status="BLOCKED", exit_code=2,
|
|
debug={"cobol_build": {"log": "not found"}})
|
|
return VerificationRun(status="PASS")
|
|
h = RetryHandler(max_heal=3, max_simple=1)
|
|
vr = h.run(fn)
|
|
assert vr.status == "PASS"
|
|
assert vr.heal_retry > 0
|
|
|
|
@test("DEFECT-004:COPYBOOKファイル名不一致", "DEFECT")
|
|
def _():
|
|
"""修复: COPY BBBBBFC (5B+FC) の解決"""
|
|
from cobol_testgen.read import resolve_copybooks
|
|
src = " COPY BBBBBFC REPLACING ==(A)== BY ==R01==."
|
|
# copybookファイルがなくてもクラッシュしない
|
|
result = resolve_copybooks(src, "/nonexistent")
|
|
assert result is not None
|
|
|
|
@test("DEFECT-005:Lark VALUE句解析", "DEFECT")
|
|
def _():
|
|
"""修复: VALUE '文字' のLark解析"""
|
|
from cobol_testgen import extract_structure
|
|
src = "IDENTIFICATION DIVISION.\nPROGRAM-ID. X.\nDATA DIVISION.\nWORKING-STORAGE SECTION.\n01 A PIC X(10) VALUE 'TEST'.\nPROCEDURE DIVISION.\nGOBACK."
|
|
s = extract_structure(src)
|
|
assert s is not None
|
|
|
|
@test("DEFECT-006:OPEN方向OUTPUT誤認識", "DEFECT")
|
|
def _():
|
|
"""修复: OPEN方向キーワードがファイル名に含まれない"""
|
|
from cobol_testgen.read import scan_open_statements
|
|
src = "OPEN INPUT TRANS-FILE.\nOPEN OUTPUT OUTPUT-FILE."
|
|
dirs = scan_open_statements(src)
|
|
# 'OUTPUT'は方向キーワードとして除外され、ファイル名にはならない
|
|
assert 'OUTPUT' not in dirs # キーワードはフィルタされる
|
|
assert 'OUTPUT-FILE' in dirs
|
|
assert dirs['OUTPUT-FILE'] == 'OUTPUT'
|
|
|
|
@test("DEFECT-007:Enum値一致判定", "DEFECT")
|
|
def _():
|
|
"""HINA分類のmethodキー存在確認"""
|
|
from hina.classifier import compute_confidence
|
|
r = compute_confidence("EXEC SQL SELECT\nEND-EXEC.")
|
|
assert "method" in r
|
|
assert r["method"] == "keyword"
|
|
r2 = compute_confidence("DISPLAY 'X'.")
|
|
assert r2["method"] == "none"
|
|
|
|
@test("DEFECT-008:machine_json全フィールド", "DEFECT")
|
|
def _():
|
|
"""P5修复: machine_jsonに全フィールド含む"""
|
|
from report.generator import ReportGenerator
|
|
vr = VerificationRun(program="TEST", branch_rate=0.9, paragraph_rate=0.8,
|
|
quality_score=0.85, hina_type="M", hina_confidence=0.95)
|
|
rd = Path(tempfile.mkdtemp())
|
|
try:
|
|
p = ReportGenerator().generate_machine_json(vr, rd / "m.json")
|
|
d = json.loads(p.read_text())
|
|
assert "branch_rate" in d
|
|
assert "paragraph_rate" in d
|
|
assert "quality_score" in d
|
|
assert "hina_type" in d
|
|
finally:
|
|
shutil.rmtree(rd)
|
|
|
|
# ════════════════════════════════════════════
|
|
# 集計
|
|
# ════════════════════════════════════════════
|
|
|
|
section("テスト結果集計")
|
|
total = PASS + FAIL
|
|
print(f"\n 総テスト数: {total}")
|
|
print(f" 合格: {PASS}")
|
|
print(f" 不合格: {FAIL}")
|
|
print(f" 合格率: {PASS/max(total,1)*100:.1f}%")
|
|
print(f"\n RESULT: {'ALL PASSED' if FAIL==0 else 'SOME FAILED'}")
|
|
|
|
if ERRORS:
|
|
print(f"\n 失敗詳細:")
|
|
for e in ERRORS:
|
|
print(f" ❌ {e}")
|
|
|
|
sys.exit(0 if FAIL == 0 else 1)
|