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,213 @@
|
||||
"""E2E Tests for COBOL->Java Verification Platform
|
||||
|
||||
Run: cd D:/cobol-java/v3-gstack-code-gen && python -m pytest tests/e2e/ -v
|
||||
Requires: web server on http://127.0.0.1:8000, WSL available
|
||||
"""
|
||||
import json, os, sys, time, uuid, shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT = Path(__file__).parent.parent.parent.resolve()
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
TASKS_DIR = PROJECT / "tasks"
|
||||
UPLOADS_DIR = PROJECT / "uploads"
|
||||
FIXTURES = PROJECT / "tests" / "fixtures"
|
||||
TEST_FILES = PROJECT.parent / "test-files"
|
||||
|
||||
|
||||
def _wsl(cmd: str, timeout: int = 60) -> str:
|
||||
import subprocess
|
||||
r = subprocess.run(["wsl", "bash", "-c", cmd],
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
return r.stdout + r.stderr
|
||||
|
||||
|
||||
def create_task(copybook: str, cobol: str, java_dir: str, mapping: str, runner="native") -> str:
|
||||
tid = uuid.uuid4().hex[:8]
|
||||
task_dir = UPLOADS_DIR / tid
|
||||
task_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy(copybook, task_dir / "copybook.cpy")
|
||||
shutil.copy(cobol, task_dir / "program.cbl")
|
||||
shutil.copy(mapping, task_dir / "mapping.yaml")
|
||||
java_dst = task_dir / "java"
|
||||
if Path(java_dir).is_dir():
|
||||
if java_dst.exists():
|
||||
shutil.rmtree(java_dst)
|
||||
shutil.copytree(java_dir, java_dst)
|
||||
else:
|
||||
shutil.copy(java_dir, java_dst)
|
||||
task = {
|
||||
"id": tid, "status": "queued",
|
||||
"copybook": f"uploads\\{tid}\\copybook.cpy",
|
||||
"cobol_src": f"uploads\\{tid}\\program.cbl",
|
||||
"java_src": f"uploads\\{tid}\\java",
|
||||
"mapping": f"uploads\\{tid}\\mapping.yaml",
|
||||
"runner": runner, "created": datetime.now().isoformat(),
|
||||
}
|
||||
(TASKS_DIR / f"{tid}.json").write_text(json.dumps(task))
|
||||
return tid
|
||||
|
||||
|
||||
def run_worker_for_task(tid: str):
|
||||
script = (
|
||||
"cd /mnt/d/cobol-java/v3-gstack-code-gen && "
|
||||
"export LLM_API_KEY=sk-ca4961087c7f4aefa8ed0fc6f3d02329 && "
|
||||
"export LLM_API_BASE=https://api.deepseek.com/v1 && "
|
||||
"export LLM_MODEL=deepseek-chat && "
|
||||
f"python3 -c \"exec(open('write_result.py').read().replace('ec17bf32','{tid}'))\""
|
||||
)
|
||||
out = _wsl(script, timeout=90)
|
||||
return out
|
||||
|
||||
|
||||
class TestPipelineE2E:
|
||||
"""End-to-end pipeline tests with Playwright browser verification."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def browser(self, page):
|
||||
self.page = page
|
||||
yield
|
||||
self.page = None
|
||||
|
||||
def test_result_page_summary(self):
|
||||
"""Task processed in WSL → result page shows correct summary."""
|
||||
tid = "75bf0dfe" # pre-processed PASS task
|
||||
self.page.goto(f"{BASE_URL}/result/{tid}")
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
self.page.screenshot(path=str(PROJECT.parent / "screenshots" / "e2e-summary.png"), full_page=True)
|
||||
|
||||
status = self.page.locator("dt:has-text('Status') + dd").first.text_content()
|
||||
matched = self.page.locator("dt:has-text('Matched') + dd").first.text_content()
|
||||
mismatched = self.page.locator("dt:has-text('Mismatched') + dd").first.text_content()
|
||||
|
||||
assert status == "PASS", f"Expected PASS, got {status}"
|
||||
assert matched == "3", f"Expected 3 matched, got {matched}"
|
||||
assert mismatched == "0", f"Expected 0 mismatched, got {mismatched}"
|
||||
|
||||
def test_result_page_field_table(self):
|
||||
"""Field results table shows correct per-field status."""
|
||||
tid = "75bf0dfe"
|
||||
self.page.goto(f"{BASE_URL}/result/{tid}")
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
rows = self.page.locator("table tr").all()
|
||||
field_data = {}
|
||||
for row in rows:
|
||||
cells = row.locator("td").all()
|
||||
if len(cells) >= 3:
|
||||
name = cells[0].text_content().strip()
|
||||
status = cells[1].text_content().strip()
|
||||
cobol_val = cells[2].text_content().strip()
|
||||
java_val = cells[3].text_content().strip() if len(cells) > 3 else ""
|
||||
if name in ("BR-AMT", "BR-STATUS", "BR-DATE"):
|
||||
field_data[name] = (status, cobol_val, java_val)
|
||||
|
||||
assert field_data["BR-AMT"][0] == "PASS"
|
||||
assert "1500" in field_data["BR-AMT"][1] or "1500" in field_data["BR-AMT"][2]
|
||||
assert field_data["BR-STATUS"][1] == field_data["BR-STATUS"][2] == "A"
|
||||
assert field_data["BR-DATE"][1] == field_data["BR-DATE"][2] == "20260522"
|
||||
self.page.screenshot(path=str(PROJECT.parent / "screenshots" / "e2e-field-table.png"), full_page=True)
|
||||
|
||||
def test_result_page_fieldtree(self):
|
||||
"""Pipeline details section shows COPYBOOK FieldTree."""
|
||||
tid = "75bf0dfe"
|
||||
self.page.goto(f"{BASE_URL}/result/{tid}")
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
tree_text = self.page.locator("h3:has-text('COPYBOOK FieldTree') + table").text_content()
|
||||
assert "CUST-ID" in tree_text
|
||||
assert "BR-AMT" in tree_text
|
||||
assert "COMP-3" in tree_text
|
||||
|
||||
def test_status_api(self):
|
||||
"""Status API returns correct JSON."""
|
||||
tid = "75bf0dfe"
|
||||
self.page.goto(f"{BASE_URL}/status/{tid}")
|
||||
body = self.page.locator("body").text_content()
|
||||
data = json.loads(body)
|
||||
assert data["task_id"] == tid
|
||||
assert data["status"] == "done"
|
||||
assert data["result"]["status"] == "PASS"
|
||||
|
||||
def test_fields_api(self):
|
||||
"""Fields API returns per-field results."""
|
||||
tid = "75bf0dfe"
|
||||
self.page.goto(f"{BASE_URL}/fields/{tid}")
|
||||
body = self.page.locator("body").text_content()
|
||||
data = json.loads(body)
|
||||
assert data["task_id"] == tid
|
||||
assert len(data["fields"]) >= 3
|
||||
|
||||
def test_home_page_loads(self):
|
||||
"""Home page loads with all form elements."""
|
||||
self.page.goto(BASE_URL)
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
title = self.page.title()
|
||||
assert "COBOL" in title
|
||||
|
||||
buttons = self.page.locator("button").all_text_contents()
|
||||
assert any("verify" in b.lower() for b in buttons)
|
||||
|
||||
def test_result_navigation_loop(self):
|
||||
"""Result page → New Verification → Home page."""
|
||||
tid = "75bf0dfe"
|
||||
self.page.goto(f"{BASE_URL}/result/{tid}")
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
self.page.locator("a:has-text('New Verification')").click()
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
assert self.page.url == BASE_URL + "/"
|
||||
|
||||
def test_new_task_full_pipeline(self):
|
||||
"""Create task → WSL worker → verify result page."""
|
||||
tid = create_task(
|
||||
str(FIXTURES / "simple.cpy"),
|
||||
str(FIXTURES / "simple.cbl"),
|
||||
str(TEST_FILES / "java"),
|
||||
str(FIXTURES / "simple.yaml"),
|
||||
)
|
||||
out = run_worker_for_task(tid)
|
||||
|
||||
self.page.goto(f"{BASE_URL}/result/{tid}")
|
||||
self.page.wait_for_load_state("networkidle")
|
||||
|
||||
status = self.page.locator("dt:has-text('Status') + dd").first.text_content()
|
||||
assert status in ("PASS", "MISMATCH", "BLOCKED"), f"Unexpected status: {status}"
|
||||
|
||||
if status == "PASS":
|
||||
matched = self.page.locator("dt:has-text('Matched') + dd").first.text_content()
|
||||
assert matched == "3"
|
||||
|
||||
|
||||
def test_create_task_and_verify():
|
||||
"""Non-browser test: create task, run worker, check status API."""
|
||||
tid = create_task(
|
||||
str(FIXTURES / "simple.cpy"),
|
||||
str(FIXTURES / "simple.cbl"),
|
||||
str(TEST_FILES / "java"),
|
||||
str(FIXTURES / "simple.yaml"),
|
||||
)
|
||||
out = run_worker_for_task(tid)
|
||||
assert "Task updated" in out or "PASS" in out or "MISMATCH" in out, f"Worker failed: {out[-300:]}"
|
||||
|
||||
tf = TASKS_DIR / f"{tid}.json"
|
||||
data = json.loads(tf.read_text(encoding="utf-8-sig"))
|
||||
assert data["status"] == "done"
|
||||
assert data["result"]["status"] in ("PASS", "MISMATCH", "BLOCKED")
|
||||
|
||||
|
||||
def test_create_task_fails_with_invalid_cobol():
|
||||
"""Invalid COBOL → BLOCKED status."""
|
||||
tid = create_task(
|
||||
str(FIXTURES / "simple.cpy"),
|
||||
str(FIXTURES / "simple.cpy"), # wrong: COPYBOOK, not COBOL source
|
||||
str(TEST_FILES / "java"),
|
||||
str(FIXTURES / "simple.yaml"),
|
||||
)
|
||||
out = run_worker_for_task(tid)
|
||||
tf = TASKS_DIR / f"{tid}.json"
|
||||
data = json.loads(tf.read_text(encoding="utf-8-sig"))
|
||||
assert data["result"]["status"] in ("BLOCKED", "ERROR")
|
||||
Reference in New Issue
Block a user