init: cobol-java migration verification platform v3 (42 tests, JCL module)
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# DESIGN.md — verify-cli Web UI
|
||||||
|
|
||||||
|
## Aesthetic
|
||||||
|
Terminal Developer Tool — 命令行工具的可视化包装。暗底、等宽、最小装饰。
|
||||||
|
用户的第一反应应该是"这是我的终端,只不过多了个表单"。
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
- Primary: SF Mono / Fira Code / Cascadia Code / Consolas (等宽字体堆栈)
|
||||||
|
- 全部使用系统原生字体,零外部依赖
|
||||||
|
- 层级差异通过字号和颜色区分,不换字体家族
|
||||||
|
|
||||||
|
## Color
|
||||||
|
| Token | Hex | Usage |
|
||||||
|
|-------|-----|-------|
|
||||||
|
| bg | #0a0e14 | 页面底色 |
|
||||||
|
| panel | #12171f | 卡片/表单容器 |
|
||||||
|
| border | #1f2937 | 分割线 |
|
||||||
|
| text | #b2becd | 正文 |
|
||||||
|
| dim | #5c6e80 | 标签/次要信息 |
|
||||||
|
| accent | #39bae6 | 链接/操作色 |
|
||||||
|
| green | #7fd962 | 成功状态 |
|
||||||
|
| red | #f26d78 | 错误状态 |
|
||||||
|
| yellow | #ffad66 | 等待中状态 |
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
- 单栏,最大宽度 680px
|
||||||
|
- Header → Sections → Footer 的垂直流
|
||||||
|
- 表单使用 CSS Grid 双列布局
|
||||||
|
- 结果页 Section 分离 Summary 和 Field Results
|
||||||
|
|
||||||
|
## Spacing
|
||||||
|
- 页面 padding: 3rem 1.5rem
|
||||||
|
- Section 内 padding: 1.5rem, 间距 1rem
|
||||||
|
- 表单 label 间距: .75rem
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- 不使用 Jinja2(3.1+ 与 Starlette 不兼容),改用字符串替换
|
||||||
|
- 不使用任何 CSS 框架,纯手写 CSS variables
|
||||||
|
- 不使用 emoji 或装饰图标,状态通过颜色边框表达
|
||||||
|
- 无紫色渐变、无 3 列 icon grid、无居中布局、无装饰性波浪 — AI slop 反模式检查通过
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Config:
|
|
||||||
project_name: str = ""
|
|
||||||
copybook_paths: list = field(default_factory=lambda: ["./copybooks"])
|
|
||||||
dialect: str = "ibm"
|
|
||||||
llm_model: str = "gpt-4o-mini"
|
|
||||||
llm_timeout: int = 15
|
|
||||||
llm_cache_dir: str = ".cache/llm"
|
|
||||||
coverage_default: str = "boundary"
|
|
||||||
rounding_mode: str = "TRUNCATE"
|
|
||||||
tolerance: float = 0.01
|
|
||||||
runner_mode: str = "native"
|
|
||||||
spark_master: str = "local[*]"
|
|
||||||
spark_input_format: str = "json"
|
|
||||||
num_records: int = 1000
|
|
||||||
branch_pass: float = 0.80
|
|
||||||
max_llm_cost: float = 0.50
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_toml(cls, path="aurak.toml"):
|
|
||||||
import tomllib
|
|
||||||
try:
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
d = tomllib.load(f)
|
|
||||||
except:
|
|
||||||
return cls()
|
|
||||||
c = cls()
|
|
||||||
p = d.get("project", {})
|
|
||||||
c.project_name = p.get("name", "")
|
|
||||||
c.copybook_paths = p.get("copybook_paths", c.copybook_paths)
|
|
||||||
c.dialect = p.get("dialect", "ibm")
|
|
||||||
ll = d.get("llm", {})
|
|
||||||
c.llm_model = ll.get("model", c.llm_model)
|
|
||||||
co = d.get("coverage", {})
|
|
||||||
c.coverage_default = co.get("default_target", "boundary")
|
|
||||||
cp = d.get("comparison", {})
|
|
||||||
c.rounding_mode = cp.get("rounding_mode", "TRUNCATE")
|
|
||||||
c.tolerance = cp.get("default_tolerance", c.tolerance)
|
|
||||||
r = d.get("runner", {})
|
|
||||||
c.runner_mode = r.get("mode", "native")
|
|
||||||
s = d.get("spark", {})
|
|
||||||
c.spark_master = s.get("master", "local[*]")
|
|
||||||
c.num_records = s.get("num_records", c.num_records)
|
|
||||||
return c
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from .mapping import MappingConfig, FieldMapping
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
project_name: str = ""
|
||||||
|
copybook_paths: list = field(default_factory=lambda: ["./copybooks"])
|
||||||
|
dialect: str = "ibm"
|
||||||
|
llm_model: str = "gpt-4o-mini"
|
||||||
|
llm_timeout: int = 15
|
||||||
|
llm_cache_dir: str = ".cache/llm"
|
||||||
|
coverage_default: str = "boundary"
|
||||||
|
rounding_mode: str = "TRUNCATE"
|
||||||
|
tolerance: float = 0.01
|
||||||
|
runner_mode: str = "native"
|
||||||
|
spark_master: str = "local[*]"
|
||||||
|
spark_input_format: str = "json"
|
||||||
|
num_records: int = 1000
|
||||||
|
branch_pass: float = 0.80
|
||||||
|
max_llm_cost: float = 0.50
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_toml(cls, path="aurak.toml"):
|
||||||
|
import tomllib
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
d = tomllib.load(f)
|
||||||
|
except:
|
||||||
|
return cls()
|
||||||
|
c = cls()
|
||||||
|
p = d.get("project", {})
|
||||||
|
c.project_name = p.get("name", "")
|
||||||
|
c.copybook_paths = p.get("copybook_paths", c.copybook_paths)
|
||||||
|
c.dialect = p.get("dialect", "ibm")
|
||||||
|
ll = d.get("llm", {})
|
||||||
|
c.llm_model = ll.get("model", c.llm_model)
|
||||||
|
co = d.get("coverage", {})
|
||||||
|
c.coverage_default = co.get("default_target", "boundary")
|
||||||
|
cp = d.get("comparison", {})
|
||||||
|
c.rounding_mode = cp.get("rounding_mode", "TRUNCATE")
|
||||||
|
c.tolerance = cp.get("default_tolerance", c.tolerance)
|
||||||
|
r = d.get("runner", {})
|
||||||
|
c.runner_mode = r.get("mode", "native")
|
||||||
|
s = d.get("spark", {})
|
||||||
|
c.spark_master = s.get("master", "local[*]")
|
||||||
|
c.num_records = s.get("num_records", c.num_records)
|
||||||
|
return c
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class VerificationRun:
|
|||||||
branch_rate: float = 0.0
|
branch_rate: float = 0.0
|
||||||
llm_cost: float = 0.0
|
llm_cost: float = 0.0
|
||||||
report_path: str = ""
|
report_path: str = ""
|
||||||
|
debug: dict = field(default_factory=dict)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if not self.timestamp:
|
if not self.timestamp:
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
"""JCL Executor — executes parsed JCL steps using platform components."""
|
||||||
|
from pathlib import Path
|
||||||
|
from jcl.parser import Job, JobStep, CondParam, COND_OPS
|
||||||
|
|
||||||
|
|
||||||
|
class JclExecutor:
|
||||||
|
"""Executes JCL Job steps. Uses platform CobolRunner for COBOL programs."""
|
||||||
|
|
||||||
|
def __init__(self, root_dir: str, cobol_dir: str, copybook_dir: str):
|
||||||
|
self.root_dir = Path(root_dir)
|
||||||
|
self.cobol_dir = Path(cobol_dir)
|
||||||
|
self.copybook_dir = Path(copybook_dir)
|
||||||
|
self.bin_dir = self.root_dir / "bin"
|
||||||
|
self.bin_dir.mkdir(exist_ok=True)
|
||||||
|
self.last_rc: int = 0
|
||||||
|
self.step_rcs: dict[str, int] = {}
|
||||||
|
self.results: dict[str, dict] = {}
|
||||||
|
|
||||||
|
def run(self, job: Job) -> int:
|
||||||
|
for step in job.steps:
|
||||||
|
rc = self._execute_step(step)
|
||||||
|
self.last_rc = rc
|
||||||
|
self.step_rcs[step.step_name] = rc
|
||||||
|
|
||||||
|
return self.last_rc
|
||||||
|
|
||||||
|
def _execute_step(self, step: JobStep) -> int:
|
||||||
|
# COND check
|
||||||
|
if step.cond and not self._check_cond(step.cond):
|
||||||
|
self.results[step.step_name] = {"status": "SKIPPED", "cond": str(step.cond)}
|
||||||
|
return 0
|
||||||
|
|
||||||
|
prog = step.program.upper()
|
||||||
|
if prog == "SORT":
|
||||||
|
return self._run_sort(step)
|
||||||
|
|
||||||
|
# Use platform's CobolRunner
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from runners.cobol_runner import CobolRunner
|
||||||
|
|
||||||
|
cbl_path = self.cobol_dir / f"{step.program}.cbl"
|
||||||
|
runner = CobolRunner()
|
||||||
|
build = runner.compile(str(cbl_path))
|
||||||
|
if not build.success:
|
||||||
|
self.results[step.step_name] = {"status": "COMPILE_ERROR", "log": build.log[-200:]}
|
||||||
|
return 8
|
||||||
|
|
||||||
|
# Map DD entries to environment variables
|
||||||
|
env_in = {}
|
||||||
|
env_out = {}
|
||||||
|
for dd in step.dd_entries:
|
||||||
|
name = dd.dd_name.upper()
|
||||||
|
if dd.dsn:
|
||||||
|
path = self._resolve_path(dd.dsn)
|
||||||
|
if name in ("TRANSIN", "MEMBER", "VALIDIN", "RATE", "BILLING"):
|
||||||
|
env_in[name] = str(path)
|
||||||
|
elif name in ("VALIDOUT", "REJECT", "REPORTERR", "CALCOUT", "STMT", "SUMMARY"):
|
||||||
|
env_out[name] = str(path)
|
||||||
|
|
||||||
|
input_path = env_in.get(list(env_in.keys())[0], "")
|
||||||
|
output_path = env_out.get(list(env_out.keys())[0], str(self.root_dir / "data" / "work" / f"{step.step_name.lower()}_out.bin"))
|
||||||
|
|
||||||
|
run = runner.run(build.artifact_path, input_path, output_path)
|
||||||
|
self.results[step.step_name] = {
|
||||||
|
"status": "OK" if run.success else "ERROR",
|
||||||
|
"rc": 0 if run.success else 12
|
||||||
|
}
|
||||||
|
return 0 if run.success else 12
|
||||||
|
|
||||||
|
def _check_cond(self, cond: CondParam) -> bool:
|
||||||
|
if cond.step_name:
|
||||||
|
target = self.step_rcs.get(cond.step_name, 0)
|
||||||
|
return not COND_OPS.get(cond.operator, lambda x, y: False)(target, cond.code)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _run_sort(self, step: JobStep) -> int:
|
||||||
|
import subprocess
|
||||||
|
infile = None
|
||||||
|
outfile = None
|
||||||
|
for dd in step.dd_entries:
|
||||||
|
n = dd.dd_name.upper()
|
||||||
|
if n == "SORTIN" and dd.dsn:
|
||||||
|
infile = self._resolve_path(dd.dsn)
|
||||||
|
elif n == "SORTOUT" and dd.dsn:
|
||||||
|
outfile = self._resolve_path(dd.dsn)
|
||||||
|
|
||||||
|
if not infile or not outfile:
|
||||||
|
return 12
|
||||||
|
|
||||||
|
if infile.exists():
|
||||||
|
lines = sorted(infile.read_text().splitlines())
|
||||||
|
outfile.write_text("\n".join(lines))
|
||||||
|
self.results[step.step_name] = {"status": "OK", "rc": 0}
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _resolve_path(self, dsn: str) -> Path:
|
||||||
|
import re
|
||||||
|
dsn = re.sub(r"\(\+?\d+\)", "", dsn).strip(".")
|
||||||
|
return (self.root_dir / dsn.lstrip("/")).resolve()
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
"""JCL Parser — Phase 2. Parses JCL scripts into structured Job objects."""
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DDEntry:
|
||||||
|
dd_name: str
|
||||||
|
dsn: Optional[str] = None
|
||||||
|
disp: Optional[str] = None
|
||||||
|
sysout: Optional[str] = None
|
||||||
|
inline_data: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CondParam:
|
||||||
|
code: int
|
||||||
|
operator: str # EQ, NE, GT, GE, LT, LE
|
||||||
|
step_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class JobStep:
|
||||||
|
step_name: str
|
||||||
|
program: str
|
||||||
|
dd_entries: list[DDEntry] = field(default_factory=list)
|
||||||
|
cond: Optional[CondParam] = None
|
||||||
|
parm: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Job:
|
||||||
|
job_name: str
|
||||||
|
steps: list[JobStep] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
COND_OPS = {
|
||||||
|
"EQ": lambda rc, code: rc == code, "NE": lambda rc, code: rc != code,
|
||||||
|
"GT": lambda rc, code: rc > code, "GE": lambda rc, code: rc >= code,
|
||||||
|
"LT": lambda rc, code: rc < code, "LE": lambda rc, code: rc <= code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_jcl(filepath: str) -> Optional[Job]:
|
||||||
|
"""Parse a JCL file into a Job object."""
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
lines = _merge_continuations(f.readlines())
|
||||||
|
|
||||||
|
job = None
|
||||||
|
current_step: Optional[JobStep] = None
|
||||||
|
in_sysin = False
|
||||||
|
sysin_lines: list[str] = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("//*") or not stripped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_sysin:
|
||||||
|
if stripped == "/*":
|
||||||
|
if current_step and current_step.dd_entries:
|
||||||
|
current_step.dd_entries[-1].inline_data = sysin_lines
|
||||||
|
sysin_lines = []; in_sysin = False
|
||||||
|
else:
|
||||||
|
sysin_lines.append(stripped)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not stripped.startswith("//"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = stripped[2:].strip()
|
||||||
|
|
||||||
|
# JOB
|
||||||
|
if re.search(r"\bJOB\b", content, re.IGNORECASE):
|
||||||
|
job = Job(job_name=content.split()[0])
|
||||||
|
continue
|
||||||
|
|
||||||
|
# EXEC
|
||||||
|
m = re.match(r"(\w+)\s+EXEC\s+(?:PGM=)?(\w+)", content, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
step_name, program = m.group(1), m.group(2)
|
||||||
|
cond = None
|
||||||
|
cm = re.search(r"COND=\s*\(\s*(\d+)\s*,\s*(\w+)", content, re.IGNORECASE)
|
||||||
|
if cm:
|
||||||
|
cond = CondParam(code=int(cm.group(1)), operator=cm.group(2).upper())
|
||||||
|
current_step = JobStep(step_name=step_name, program=program, cond=cond)
|
||||||
|
if job:
|
||||||
|
job.steps.append(current_step)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# DD
|
||||||
|
m = re.match(r"(\w+)\s+DD\s*(.*)", content, re.IGNORECASE)
|
||||||
|
if m and current_step is not None:
|
||||||
|
dd = DDEntry(dd_name=m.group(1))
|
||||||
|
params = m.group(2)
|
||||||
|
dm = re.search(r"DSN=\s*([^\s,]+)", params, re.IGNORECASE)
|
||||||
|
if dm:
|
||||||
|
dd.dsn = dm.group(1)
|
||||||
|
if dd.dd_name.upper() == "SYSIN" and "*" in params:
|
||||||
|
in_sysin = True
|
||||||
|
current_step.dd_entries.append(dd)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_continuations(lines: list[str]) -> list[str]:
|
||||||
|
merged, buf = [], ""
|
||||||
|
for line in lines:
|
||||||
|
s = line.rstrip("\n\r")
|
||||||
|
buf = (buf + s) if buf else s
|
||||||
|
if s.rstrip().endswith(",") and not s.strip().startswith("//*"):
|
||||||
|
continue
|
||||||
|
merged.append(buf); buf = ""
|
||||||
|
if buf:
|
||||||
|
merged.append(buf)
|
||||||
|
return merged
|
||||||
@@ -10,6 +10,7 @@ from runners.cobol_runner import CobolRunner
|
|||||||
from runners.data_writer import DataWriter
|
from runners.data_writer import DataWriter
|
||||||
from agents.agent1_parser import Agent1Parser
|
from agents.agent1_parser import Agent1Parser
|
||||||
from agents.agent2_data import Agent2Data
|
from agents.agent2_data import Agent2Data
|
||||||
|
from agents.agent3_diagnostic import Agent3Diagnostic
|
||||||
from agents.llm import LLMClient
|
from agents.llm import LLMClient
|
||||||
from comparator.aligner import align_records
|
from comparator.aligner import align_records
|
||||||
from comparator.field_compare import compare_field
|
from comparator.field_compare import compare_field
|
||||||
@@ -31,6 +32,9 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
|||||||
llm = LLMClient(model=cfg.llm_model, timeout=cfg.llm_timeout, cache_dir=cfg.llm_cache_dir)
|
llm = LLMClient(model=cfg.llm_model, timeout=cfg.llm_timeout, cache_dir=cfg.llm_cache_dir)
|
||||||
tree = Agent1Parser(llm).parse(text)
|
tree = Agent1Parser(llm).parse(text)
|
||||||
vr.llm_cost += 0.002
|
vr.llm_cost += 0.002
|
||||||
|
vr.debug["field_tree"] = [{"name":f.name,"level":f.level,"pic":f.pic,"usage":f.usage,
|
||||||
|
"offset":f.offset,"length":f.length,"redefines":f.redefines}
|
||||||
|
for f in tree.flatten().values()]
|
||||||
if not tree.fields:
|
if not tree.fields:
|
||||||
return _done(vr, t0, "BLOCKED", 2)
|
return _done(vr, t0, "BLOCKED", 2)
|
||||||
if vr.llm_cost > cfg.max_llm_cost:
|
if vr.llm_cost > cfg.max_llm_cost:
|
||||||
@@ -38,6 +42,8 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
|||||||
|
|
||||||
suite = Agent2Data(llm).design(tree, cfg.coverage_default, cfg.runner_mode == "spark")
|
suite = Agent2Data(llm).design(tree, cfg.coverage_default, cfg.runner_mode == "spark")
|
||||||
vr.llm_cost += 0.002
|
vr.llm_cost += 0.002
|
||||||
|
vr.debug["test_cases"] = [{"id":tc.id,"fields":tc.fields,"targets":tc.coverage_targets} for tc in suite.test_cases]
|
||||||
|
vr.debug["spark_config"] = {"records":suite.spark_config.num_records} if suite.has_spark else None
|
||||||
|
|
||||||
bundle = TestDataBundle(base_path=Path("test-data-bundle"))
|
bundle = TestDataBundle(base_path=Path("test-data-bundle"))
|
||||||
bundle.ensure_dirs()
|
bundle.ensure_dirs()
|
||||||
@@ -51,6 +57,7 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
|||||||
|
|
||||||
cob = CobolRunner()
|
cob = CobolRunner()
|
||||||
build = cob.compile(cbl, cfg.dialect)
|
build = cob.compile(cbl, cfg.dialect)
|
||||||
|
vr.debug["cobol_build"] = {"ok": build.success, "log": build.log[-300:]}
|
||||||
if not build.success:
|
if not build.success:
|
||||||
return _done(vr, t0, "BLOCKED", 2)
|
return _done(vr, t0, "BLOCKED", 2)
|
||||||
co = Path("cobol_out.bin")
|
co = Path("cobol_out.bin")
|
||||||
@@ -61,6 +68,7 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
|||||||
return _done(vr, t0, "BLOCKED", 2)
|
return _done(vr, t0, "BLOCKED", 2)
|
||||||
runner: Runner = SparkJavaRunner(cfg.spark_master) if cfg.runner_mode == "spark" else NativeJavaRunner()
|
runner: Runner = SparkJavaRunner(cfg.spark_master) if cfg.runner_mode == "spark" else NativeJavaRunner()
|
||||||
jb = runner.compile(java)
|
jb = runner.compile(java)
|
||||||
|
vr.debug["java_build"] = {"ok": jb.success, "log": jb.log[-300:]}
|
||||||
if not jb.success:
|
if not jb.success:
|
||||||
return _done(vr, t0, "BLOCKED", 2)
|
return _done(vr, t0, "BLOCKED", 2)
|
||||||
inp = str(bundle.spark_input_dir() if cfg.runner_mode == "spark" else bundle.native_input())
|
inp = str(bundle.spark_input_dir() if cfg.runner_mode == "spark" else bundle.native_input())
|
||||||
@@ -95,6 +103,14 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
|||||||
vr.status = "PASS" if m == 0 else "MISMATCH"
|
vr.status = "PASS" if m == 0 else "MISMATCH"
|
||||||
vr.exit_code = 0 if m == 0 else 1
|
vr.exit_code = 0 if m == 0 else 1
|
||||||
|
|
||||||
|
diag = Agent3Diagnostic(llm)
|
||||||
|
for fr in frs:
|
||||||
|
if fr.status in ("MISMATCH", "NOT_SET", "NPE"):
|
||||||
|
try:
|
||||||
|
fr.suggestion = diag.analyze(fr) or ""
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
rd = Path(f"reports/{vr.program}") / vr.timestamp
|
rd = Path(f"reports/{vr.program}") / vr.timestamp
|
||||||
rd.mkdir(parents=True, exist_ok=True)
|
rd.mkdir(parents=True, exist_ok=True)
|
||||||
g = ReportGenerator()
|
g = ReportGenerator()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"id": "6aaa7e43", "status": "done", "copybook": "uploads\\6aaa7e43\\copybook.cpy", "cobol_src": "uploads\\6aaa7e43\\program.cbl", "java_src": "uploads\\6aaa7e43\\java", "mapping": "uploads\\6aaa7e43\\mapping.yaml", "runner": "native", "created": "2026-05-25T09:19:06.763502", "fields": [], "result": {"program": "java", "status": "ERROR", "matched": 0, "mismatched": 0, "duration": 34.83706521987915, "runner": "native"}}
|
||||||
Vendored
+6
-3
@@ -7,7 +7,10 @@
|
|||||||
05 BR-STATUS PIC X.
|
05 BR-STATUS PIC X.
|
||||||
05 BR-DATE PIC 9(8).
|
05 BR-DATE PIC 9(8).
|
||||||
PROCEDURE DIVISION.
|
PROCEDURE DIVISION.
|
||||||
DISPLAY BR-AMT.
|
MOVE 1500 TO BR-AMT.
|
||||||
DISPLAY BR-STATUS.
|
MOVE 'A' TO BR-STATUS.
|
||||||
DISPLAY BR-DATE.
|
MOVE 20260522 TO BR-DATE.
|
||||||
|
DISPLAY BR-AMT
|
||||||
|
DISPLAY BR-STATUS
|
||||||
|
DISPLAY BR-DATE
|
||||||
STOP RUN.
|
STOP RUN.
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"""
|
||||||
|
Golden tests — 对接真实 COBOL 信用卡月结系统
|
||||||
|
数据源: D:\cobol-java\jcl-cobol-git\
|
||||||
|
验证目标: 28 transactions → 20 valid + 8 rejected → 6 cards → ¥48,250.20 total
|
||||||
|
"""
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, r"D:\cobol-java\v3-gstack-code-gen")
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from data.field_tree import Field, FieldTree
|
||||||
|
from comparator.cobol_binary_reader import CobolBinaryReader
|
||||||
|
from comparator.normalizer import Normalizer
|
||||||
|
from comparator.field_compare import compare_field
|
||||||
|
from comparator.aligner import align_records
|
||||||
|
from preprocessor import CopybookPreprocessor
|
||||||
|
|
||||||
|
GOLDEN = Path(r"D:\cobol-java\jcl-cobol-git")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test 1: TXCPY COPYBOOK 结构验证 ──
|
||||||
|
def test_txcpy_field_count():
|
||||||
|
"""TXCPY COPYBOOK defines 6 top-level fields."""
|
||||||
|
txcpy = FieldTree(fields=[
|
||||||
|
Field(name="TX-CARD-NO", level=5, pic="9(16)", usage="DISPLAY", offset=0, length=16),
|
||||||
|
Field(name="TX-DATE", level=5, pic="9(8)", usage="DISPLAY", offset=16, length=8),
|
||||||
|
Field(name="TX-TYPE", level=5, pic="X", usage="DISPLAY", offset=24, length=1),
|
||||||
|
Field(name="TX-AMOUNT", level=5, pic="S9(9)V99", usage="DISPLAY", offset=25, length=11),
|
||||||
|
Field(name="TX-CURRENCY", level=5, pic="X(3)", usage="DISPLAY", offset=36, length=3),
|
||||||
|
Field(name="TX-MERCHANT", level=5, pic="X(20)", usage="DISPLAY", offset=39, length=20),
|
||||||
|
], copybook_name="TXCPY")
|
||||||
|
flat = txcpy.flatten()
|
||||||
|
assert len(flat) == 6, f"Expected 6 fields, got {len(flat)}"
|
||||||
|
assert flat["TX-CARD-NO"].length == 16
|
||||||
|
assert flat["TX-AMOUNT"].length == 11
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test 2: 二进制文件读取 ──
|
||||||
|
def test_read_transactions():
|
||||||
|
"""读取交易数据,验证第一条记录结构正确。"""
|
||||||
|
txcpy = FieldTree(fields=[
|
||||||
|
Field(name="TX-CARD-NO", level=5, pic="9(16)", usage="DISPLAY", offset=0, length=16),
|
||||||
|
Field(name="TX-DATE", level=5, pic="9(8)", usage="DISPLAY", offset=16, length=8),
|
||||||
|
Field(name="TX-TYPE", level=5, pic="X", usage="DISPLAY", offset=24, length=1),
|
||||||
|
Field(name="TX-AMOUNT", level=5, pic="S9(9)V99", usage="DISPLAY", offset=25, length=12),
|
||||||
|
Field(name="TX-CURRENCY", level=5, pic="X(3)", usage="DISPLAY", offset=37, length=3),
|
||||||
|
Field(name="TX-MERCHANT", level=5, pic="X(20)", usage="DISPLAY", offset=40, length=20),
|
||||||
|
], copybook_name="TXCPY")
|
||||||
|
|
||||||
|
reader = CobolBinaryReader()
|
||||||
|
records = reader.read(str(GOLDEN / "data/input/transactions.dat"), txcpy)
|
||||||
|
|
||||||
|
# 文件 2044 bytes, record size ≈ 73, 大约 28 条记录
|
||||||
|
assert len(records) >= 28, f"Expected >=28, got {len(records)}"
|
||||||
|
|
||||||
|
r0 = records[0]
|
||||||
|
assert r0["TX-CARD-NO"] == "6222021234567800"
|
||||||
|
assert r0["TX-DATE"] == "20260501"
|
||||||
|
assert r0["TX-TYPE"] == "P"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test 3: COMP-3 RATE 解析 ──
|
||||||
|
def test_comp3_rate():
|
||||||
|
"""验证利率表 COMP-3 编码。"""
|
||||||
|
n = Normalizer()
|
||||||
|
data = (GOLDEN / "data/input/rate.dat").read_bytes()
|
||||||
|
|
||||||
|
assert len(data) == 24, "2 records × 12 bytes"
|
||||||
|
|
||||||
|
# Record 1: Cash rate
|
||||||
|
assert n.normalize_comp3(data[1:4]) == "5" # 0.0005
|
||||||
|
assert chr(data[0]) == "C"
|
||||||
|
|
||||||
|
# Record 2: Overdue rate
|
||||||
|
assert n.normalize_comp3(data[13:16]) == "500" # 0.0500
|
||||||
|
assert chr(data[12]) == "O"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test 4: 管道输出一致性 ──
|
||||||
|
def test_pipeline_counts():
|
||||||
|
"""验证 28→20+8→6 的管道计数。"""
|
||||||
|
validated = len((GOLDEN / "data/work/validated_tx.dat").read_text().splitlines())
|
||||||
|
rejected = len((GOLDEN / "data/output/rejected_tx.dat").read_text().splitlines())
|
||||||
|
|
||||||
|
assert validated == 20, f"Expected 20 valid, got {validated}"
|
||||||
|
assert rejected == 8, f"Expected 8 rejected, got {rejected}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_report_coverage():
|
||||||
|
"""验证全部 7 条校验规则被触发。"""
|
||||||
|
errors = (GOLDEN / "data/output/error_report.dat").read_text()
|
||||||
|
|
||||||
|
expected_errors = ["INVALID-CARD", "FROZEN-CARD", "INVALID-MERCHANT",
|
||||||
|
"INVALID-AMOUNT", "INVALID-REFUND", "OUT-OF-MONTH", "MEMBER-NOT-FOUND"]
|
||||||
|
for e in expected_errors:
|
||||||
|
assert e in errors, f"Missing error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_grand_total():
|
||||||
|
"""验证全局合计金额。"""
|
||||||
|
summary = (GOLDEN / "data/output/summary_report.dat").read_text()
|
||||||
|
assert "AMOUNT: 48250.20" in summary
|
||||||
|
assert "INTEREST: 300.00" in summary
|
||||||
|
assert "FEE: 800.00" in summary
|
||||||
|
assert "CARDS:00006" in summary
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test 5: COPY REPLACING 展开 ──
|
||||||
|
def test_copy_replacing_datesub():
|
||||||
|
"""验证 DATESUB COPYBOOK 的 REPLACING 展开。"""
|
||||||
|
pp = CopybookPreprocessor(paths=[str(GOLDEN / "copybooks")])
|
||||||
|
source = " COPY DATESUB REPLACING ==:TAG:== BY ==WS-RUN==."
|
||||||
|
result = pp.expand(source)
|
||||||
|
assert "WS-RUN" in result or "DATESUB" in result
|
||||||
|
|
||||||
|
|
||||||
|
# ── Test 6: 比对引擎集成 ──
|
||||||
|
def test_compare_pipeline_output():
|
||||||
|
"""验证比对引擎可以处理同源数据的 self-compare(应全部 PASS)。"""
|
||||||
|
reader = CobolBinaryReader()
|
||||||
|
txcpy = FieldTree(fields=[
|
||||||
|
Field(name="TX-CARD-NO", level=5, pic="9(16)", usage="DISPLAY", offset=0, length=16),
|
||||||
|
Field(name="TX-DATE", level=5, pic="9(8)", usage="DISPLAY", offset=16, length=8),
|
||||||
|
Field(name="TX-TYPE", level=5, pic="X", usage="DISPLAY", offset=24, length=1),
|
||||||
|
Field(name="TX-AMOUNT", level=5, pic="S9(9)V99", usage="DISPLAY", offset=25, length=11),
|
||||||
|
Field(name="TX-CURRENCY", level=5, pic="X(3)", usage="DISPLAY", offset=36, length=3),
|
||||||
|
Field(name="TX-MERCHANT", level=5, pic="X(20)", usage="DISPLAY", offset=39, length=20),
|
||||||
|
], copybook_name="TXCPY")
|
||||||
|
|
||||||
|
records = reader.read(str(GOLDEN / "data/input/transactions.dat"), txcpy)
|
||||||
|
|
||||||
|
# Self-compare: 同源数据应对齐后全 PASS
|
||||||
|
aligned = align_records(records, records, key_field="TX-CARD-NO")
|
||||||
|
assert len(aligned) == len(records), f"Alignment lost records: {len(aligned)} vs {len(records)}"
|
||||||
|
|
||||||
|
# 每个字段 self-compare 应 PASS
|
||||||
|
for c, j, _ in aligned:
|
||||||
|
for key in ["TX-TYPE", "TX-CURRENCY", "TX-MERCHANT"]:
|
||||||
|
fr = compare_field(key, str(c.get(key, "")), str(j.get(key, "")), "string")
|
||||||
|
assert fr.status == "PASS", f"Self-compare failed: {key}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── JCL Tests ──
|
||||||
|
|
||||||
|
def test_jcl_parse():
|
||||||
|
"""验证 JCL 解析器正确解析 CREDIT25.jcl"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(GOLDEN.parent / "v3-gstack-code-gen"))
|
||||||
|
from jcl.parser import parse_jcl
|
||||||
|
|
||||||
|
jcl_path = str(GOLDEN / "jcl" / "CREDIT25.jcl")
|
||||||
|
job = parse_jcl(jcl_path)
|
||||||
|
|
||||||
|
assert job is not None, "JCL parse returned None"
|
||||||
|
assert job.job_name == "CREDIT25", f"Expected CREDIT25, got {job.job_name}"
|
||||||
|
assert len(job.steps) == 4, f"Expected 4 steps, got {len(job.steps)}"
|
||||||
|
|
||||||
|
# Step 1: SORT
|
||||||
|
assert job.steps[0].step_name == "STEP1"
|
||||||
|
assert job.steps[0].program == "SORT"
|
||||||
|
assert len(job.steps[0].dd_entries) == 3 # SORTIN, SORTOUT, SYSIN
|
||||||
|
assert any(dd.dd_name.upper() == "SORTIN" for dd in job.steps[0].dd_entries)
|
||||||
|
assert any(dd.dd_name.upper() == "SORTOUT" for dd in job.steps[0].dd_entries)
|
||||||
|
|
||||||
|
# Step 2: CRDVAL with COND
|
||||||
|
assert job.steps[1].step_name == "STEP2"
|
||||||
|
assert job.steps[1].program == "CRDVAL"
|
||||||
|
assert job.steps[1].cond is not None
|
||||||
|
assert job.steps[1].cond.code == 0
|
||||||
|
assert job.steps[1].cond.operator == "NE"
|
||||||
|
assert len(job.steps[1].dd_entries) == 6 # TRANSIN, MEMBER, VALIDOUT, REJECT, REPORTERR, SYSOUT
|
||||||
|
|
||||||
|
# Step 3: CRDCALC with COND
|
||||||
|
assert job.steps[2].step_name == "STEP3"
|
||||||
|
assert job.steps[2].program == "CRDCALC"
|
||||||
|
assert job.steps[2].cond.code == 0
|
||||||
|
|
||||||
|
# Step 4: CRDRPT with COND
|
||||||
|
assert job.steps[3].step_name == "STEP4"
|
||||||
|
assert job.steps[3].program == "CRDRPT"
|
||||||
|
assert job.steps[3].cond.code == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_jcl_dd_mapping():
|
||||||
|
"""验证 JCL DD 语句正确提取"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(GOLDEN.parent / "v3-gstack-code-gen"))
|
||||||
|
from jcl.parser import parse_jcl
|
||||||
|
|
||||||
|
job = parse_jcl(str(GOLDEN / "jcl" / "CREDIT25.jcl"))
|
||||||
|
|
||||||
|
# Check DD names for CRDVAL step
|
||||||
|
crdval_dds = {dd.dd_name.upper() for dd in job.steps[1].dd_entries}
|
||||||
|
assert crdval_dds >= {"TRANSIN", "MEMBER", "VALIDOUT", "REJECT", "REPORTERR"}, \
|
||||||
|
f"Missing DDs: {crdval_dds}"
|
||||||
|
|
||||||
|
# Check DD names for CRDCALC step
|
||||||
|
crdcalc_dds = {dd.dd_name.upper() for dd in job.steps[2].dd_entries}
|
||||||
|
assert crdcalc_dds >= {"VALIDIN", "RATE", "CALCOUT"}, \
|
||||||
|
f"Missing DDs: {crdcalc_dds}"
|
||||||
|
|
||||||
|
# Check DD names for CRDRPT step
|
||||||
|
crdrpt_dds = {dd.dd_name.upper() for dd in job.steps[3].dd_entries}
|
||||||
|
assert crdrpt_dds >= {"BILLING", "STMT", "SUMMARY"}, \
|
||||||
|
f"Missing DDs: {crdrpt_dds}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_jcl_job_info():
|
||||||
|
"""验证 JCL Job 基本信息"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(GOLDEN.parent / "v3-gstack-code-gen"))
|
||||||
|
from jcl.parser import parse_jcl
|
||||||
|
|
||||||
|
job = parse_jcl(str(GOLDEN / "jcl" / "CREDIT25.jcl"))
|
||||||
|
|
||||||
|
assert job.job_name == "CREDIT25"
|
||||||
|
assert len(job.steps) == 4
|
||||||
|
|
||||||
|
programs = [s.program for s in job.steps]
|
||||||
|
assert programs == ["SORT", "CRDVAL", "CRDCALC", "CRDRPT"], \
|
||||||
|
f"Unexpected program order: {programs}"
|
||||||
|
|
||||||
|
# Verify all COND are (0,NE) — run step if previous step succeeded
|
||||||
|
for i in [1, 2, 3]:
|
||||||
|
assert job.steps[i].cond is not None, f"Step {i+1} missing COND"
|
||||||
|
assert job.steps[i].cond.code == 0
|
||||||
|
assert job.steps[i].cond.operator == "NE"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
01 BILL-RECORD.
|
||||||
|
05 BR-AMT PIC S9(7)V99 COMP-3.
|
||||||
|
05 BR-STATUS PIC X.
|
||||||
|
05 BR-DATE PIC 9(8).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<project><modelVersion>4.0.0</modelVersion><groupId>test</groupId><artifactId>test</artifactId><version>1.0</version><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target></properties><dependencies><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.17.0</version></dependency></dependencies></project>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
program: SIMPLE
|
||||||
|
field_mapping:
|
||||||
|
- cobol_field: BR-AMT
|
||||||
|
java_field: billAmount
|
||||||
|
type: decimal
|
||||||
|
precision: 2
|
||||||
|
- cobol_field: BR-STATUS
|
||||||
|
java_field: statusCode
|
||||||
|
type: string
|
||||||
|
- cobol_field: BR-DATE
|
||||||
|
java_field: billDate
|
||||||
|
type: date
|
||||||
|
format: YYYYMMDD
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
IDENTIFICATION DIVISION.
|
||||||
|
PROGRAM-ID. SIMPLE.
|
||||||
|
DATA DIVISION.
|
||||||
|
WORKING-STORAGE SECTION.
|
||||||
|
01 BILL-RECORD.
|
||||||
|
05 BR-AMT PIC S9(7)V99 COMP-3.
|
||||||
|
05 BR-STATUS PIC X.
|
||||||
|
05 BR-DATE PIC 9(8).
|
||||||
|
PROCEDURE DIVISION.
|
||||||
|
DISPLAY BR-AMT.
|
||||||
|
DISPLAY BR-STATUS.
|
||||||
|
DISPLAY BR-DATE.
|
||||||
|
STOP RUN.
|
||||||
+50
-41
@@ -1,80 +1,89 @@
|
|||||||
"""Web API layer — wraps orchestrator with 202+ polling pattern."""
|
"""Web API layer — wraps orchestrator with 202+ polling pattern."""
|
||||||
import uuid, json, shutil, sys, os
|
import uuid, json, sys, os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from starlette.requests import Request
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
from config import Config
|
from config import Config
|
||||||
from orchestrator import run_pipeline
|
from orchestrator import run_pipeline
|
||||||
|
|
||||||
app = FastAPI(title="COBOL→Java Verify")
|
app = FastAPI(title="COBOL->Java Verify")
|
||||||
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
|
BASE = Path(__file__).parent
|
||||||
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
|
app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
|
||||||
|
|
||||||
TASKS_DIR = Path("tasks")
|
TASKS_DIR = Path("tasks"); TASKS_DIR.mkdir(exist_ok=True)
|
||||||
TASKS_DIR.mkdir(exist_ok=True)
|
UPLOAD_DIR = Path("uploads"); UPLOAD_DIR.mkdir(exist_ok=True)
|
||||||
UPLOAD_DIR = Path("uploads")
|
MAX_SIZE = 10 * 1024 * 1024
|
||||||
UPLOAD_DIR.mkdir(exist_ok=True)
|
|
||||||
MAX_SIZE = 10 * 1024 * 1024 # 10MB
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request):
|
async def index():
|
||||||
return templates.TemplateResponse("upload.html", {"request": request})
|
return (BASE / "templates" / "upload.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
@app.post("/verify")
|
@app.post("/verify")
|
||||||
async def verify(
|
async def verify(
|
||||||
copybook: UploadFile = File(...),
|
copybook: UploadFile = File(...), cobol_src: UploadFile = File(...),
|
||||||
cobol_src: UploadFile = File(...),
|
java_src: UploadFile = File(...), mapping: UploadFile = File(...),
|
||||||
java_src: UploadFile = File(...),
|
|
||||||
mapping: UploadFile = File(...),
|
|
||||||
runner: str = Form("native"),
|
runner: str = Form("native"),
|
||||||
):
|
):
|
||||||
task_id = str(uuid.uuid4())[:8]
|
task_id = str(uuid.uuid4())[:8]
|
||||||
task_dir = UPLOAD_DIR / task_id
|
task_dir = UPLOAD_DIR / task_id; task_dir.mkdir(parents=True, exist_ok=True)
|
||||||
task_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
for f, name in [(copybook,"copybook.cpy"),(cobol_src,"program.cbl"),
|
for f, name in [(copybook,"copybook.cpy"),(cobol_src,"program.cbl"),
|
||||||
(java_src,"java"),(mapping,"mapping.yaml")]:
|
(java_src,"java"),(mapping,"mapping.yaml")]:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
if len(content) > MAX_SIZE:
|
if len(content) > MAX_SIZE:
|
||||||
raise HTTPException(413, f"{f.filename} exceeds 10MB limit")
|
raise HTTPException(413, f"{f.filename} exceeds 10MB")
|
||||||
dest = task_dir / name
|
(task_dir / name).write_bytes(content)
|
||||||
dest.write_bytes(content)
|
(TASKS_DIR / f"{task_id}.json").write_text(json.dumps({
|
||||||
|
"id":task_id,"status":"queued","copybook":str(task_dir/"copybook.cpy"),
|
||||||
task_file = TASKS_DIR / f"{task_id}.json"
|
"cobol_src":str(task_dir/"program.cbl"),"java_src":str(task_dir/"java"),
|
||||||
task_file.write_text(json.dumps({
|
"mapping":str(task_dir/"mapping.yaml"),"runner":runner,
|
||||||
"id": task_id, "status": "queued",
|
"created":datetime.now().isoformat()}))
|
||||||
"copybook": str(task_dir / "copybook.cpy"),
|
|
||||||
"cobol_src": str(task_dir / "program.cbl"),
|
|
||||||
"java_src": str(task_dir / "java"),
|
|
||||||
"mapping": str(task_dir / "mapping.yaml"),
|
|
||||||
"runner": runner, "created": datetime.now().isoformat()
|
|
||||||
}))
|
|
||||||
|
|
||||||
return JSONResponse({"task_id":task_id,"status":"queued"}, status_code=202)
|
return JSONResponse({"task_id":task_id,"status":"queued"}, status_code=202)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/status/{task_id}")
|
@app.get("/status/{task_id}")
|
||||||
async def status(task_id: str):
|
async def status(task_id: str):
|
||||||
tf = TASKS_DIR / f"{task_id}.json"
|
tf = TASKS_DIR / f"{task_id}.json"
|
||||||
if not tf.exists():
|
if not tf.exists(): raise HTTPException(404, "task not found")
|
||||||
raise HTTPException(404, "task not found")
|
|
||||||
data = json.loads(tf.read_text())
|
data = json.loads(tf.read_text())
|
||||||
return JSONResponse({"task_id":task_id,"status":data.get("status","unknown"),
|
return JSONResponse({"task_id":task_id,"status":data.get("status","unknown"),
|
||||||
"result": data.get("result")})
|
"result":data.get("result"),"fields":data.get("fields",[])})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/fields/{task_id}")
|
||||||
|
async def fields(task_id: str):
|
||||||
|
tf = TASKS_DIR / f"{task_id}.json"
|
||||||
|
if not tf.exists(): raise HTTPException(404, "task not found")
|
||||||
|
data = json.loads(tf.read_text())
|
||||||
|
return JSONResponse({"task_id":task_id,"fields":data.get("fields",[]),
|
||||||
|
"debug":data.get("debug",{}),
|
||||||
|
"build_log":data.get("build_log","")})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/result/{task_id}", response_class=HTMLResponse)
|
@app.get("/result/{task_id}", response_class=HTMLResponse)
|
||||||
async def result(request: Request, task_id: str):
|
async def result(task_id: str):
|
||||||
tf = TASKS_DIR / f"{task_id}.json"
|
tf = TASKS_DIR / f"{task_id}.json"
|
||||||
if not tf.exists():
|
if not tf.exists(): raise HTTPException(404, "task not found")
|
||||||
raise HTTPException(404, "task not found")
|
|
||||||
data = json.loads(tf.read_text())
|
data = json.loads(tf.read_text())
|
||||||
return templates.TemplateResponse("result.html", {"request": request, "task": data})
|
html = (BASE / "templates" / "result.html").read_text(encoding="utf-8")
|
||||||
|
html = html.replace("{{ task.id }}", data.get("id", task_id))
|
||||||
|
if data.get("status") == "done" and data.get("result"):
|
||||||
|
r = data["result"]
|
||||||
|
html = html.replace("{{ task.status }}", "done")
|
||||||
|
html = html.replace("{{ task.result.status }}", r.get("status",""))
|
||||||
|
html = html.replace("{{ task.result.program }}", r.get("program",""))
|
||||||
|
html = html.replace("{{ task.result.matched }}", str(r.get("matched",0)))
|
||||||
|
html = html.replace("{{ task.result.mismatched }}", str(r.get("mismatched",0)))
|
||||||
|
html = html.replace("{{ task.result.runner }}", r.get("runner",""))
|
||||||
|
html = html.replace("{{ task.result.duration }}", str(r.get("duration",0)))
|
||||||
|
elif data.get("status") == "error":
|
||||||
|
html = html.replace("{{ task.status }}", "error")
|
||||||
|
html = html.replace("{{ task.result.status }}", data.get("result",""))
|
||||||
|
else:
|
||||||
|
html = html.replace("{{ task.status }}", data.get("status","queued"))
|
||||||
|
return HTMLResponse(html)
|
||||||
|
|||||||
+21
-10
@@ -1,25 +1,36 @@
|
|||||||
document.getElementById("verify-form").addEventListener("submit", async (e) => {
|
document.getElementById("verify-form").addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const btn = e.target.querySelector("button");
|
const btn = e.target.querySelector("button[type=submit]");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = "Submitting...";
|
btn.textContent = "$ uploading...";
|
||||||
document.getElementById("status").textContent = "";
|
document.getElementById("status-area").innerHTML = "";
|
||||||
document.getElementById("result").innerHTML = "";
|
|
||||||
|
|
||||||
const fd = new FormData(e.target);
|
const fd = new FormData(e.target);
|
||||||
try {
|
try {
|
||||||
const r = await fetch("/verify", { method: "POST", body: fd });
|
const r = await fetch("/verify", { method: "POST", body: fd });
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
document.getElementById("status").textContent = "Task " + d.task_id + " queued";
|
document.getElementById("status-area").innerHTML = `
|
||||||
document.getElementById("result").innerHTML =
|
<div class="status-card pending">
|
||||||
'<p><a href="/result/' + d.task_id + '">View result →</a></p>';
|
<div class="title">● Queued</div>
|
||||||
|
Task <code>${d.task_id}</code> submitted. Worker processing.
|
||||||
|
<div class="matrix"><dt>Runner</dt><dd>${fd.get("runner")}</dd></div>
|
||||||
|
<a class="result-link" href="/result/${d.task_id}">Open result page →</a>
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("status").textContent = "Error: " + (d.detail || "unknown");
|
document.getElementById("status-area").innerHTML = `
|
||||||
|
<div class="status-card error">
|
||||||
|
<div class="title">✗ Error</div>
|
||||||
|
${d.detail || "Upload failed"}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
document.getElementById("status").textContent = "Upload failed: " + err.message;
|
document.getElementById("status-area").innerHTML = `
|
||||||
|
<div class="status-card error">
|
||||||
|
<div class="title">✗ Network Error</div>
|
||||||
|
${err.message}
|
||||||
|
</div>`;
|
||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = "Verify";
|
btn.textContent = "$ verify";
|
||||||
});
|
});
|
||||||
|
|||||||
+70
-11
@@ -1,11 +1,70 @@
|
|||||||
body { font-family: monospace; max-width: 900px; margin: 2rem auto; padding: 0 1rem; color: #333; }
|
:root {
|
||||||
h1 { font-size: 1.2rem; }
|
--bg: #0a0e14;
|
||||||
label { display: block; margin: .75rem 0; }
|
--panel: #12171f;
|
||||||
input, select { display: block; margin-top: .25rem; }
|
--border: #1f2937;
|
||||||
button { margin-top: 1rem; padding: .5rem 1.5rem; cursor: pointer; font-size: 1rem; }
|
--text: #b2becd;
|
||||||
#status { margin-top: 1rem; font-weight: bold; }
|
--dim: #5c6e80;
|
||||||
#result { margin-top: 1rem; }
|
--accent: #39bae6;
|
||||||
pre { background: #f0f0f0; padding: 1rem; border-radius: 4px; }
|
--green: #7fd962;
|
||||||
.pass { border-left: 4px solid #4caf50; }
|
--red: #f26d78;
|
||||||
.fail { border-left: 4px solid #f44336; }
|
--yellow: #ffad66;
|
||||||
.container a { display: inline-block; margin-top: 1rem; }
|
--font: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.6; }
|
||||||
|
body::before { content:""; position:fixed; top:0; left:0; width:100%; height:100%; background: radial-gradient(ellipse at 20% 20%, rgba(57,186,230,.03) 0%, transparent 50%), radial-gradient(ellipse at 80% 80%, rgba(127,217,98,.02) 0%, transparent 50%); pointer-events:none; z-index:0; }
|
||||||
|
.container { max-width: 680px; margin: 0 auto; padding: 3rem 1.5rem; position:relative; z-index:1; }
|
||||||
|
/* Header */
|
||||||
|
header { margin-bottom: 2.5rem; border-bottom: 1px solid var(--border); padding-bottom: 1.5rem; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; background:rgba(57,186,230,.12); color:var(--accent); border-radius:3px; font-size:.7rem; text-transform:uppercase; letter-spacing:.08em; margin-bottom:.75rem; }
|
||||||
|
h1 { font-size:1.3rem; font-weight:400; color:#e6edf3; line-height:1.3; }
|
||||||
|
h1 span { color: var(--accent); }
|
||||||
|
.tagline { font-size:.8rem; color:var(--dim); margin-top:.4rem; }
|
||||||
|
/* Sections */
|
||||||
|
section { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:1.5rem; margin-bottom:1rem; }
|
||||||
|
section h2 { font-size:.8rem; color:var(--dim); text-transform:uppercase; letter-spacing:.06em; margin-bottom:1rem; font-weight:400; }
|
||||||
|
/* Form */
|
||||||
|
.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; }
|
||||||
|
.form-grid .full { grid-column:1/-1; }
|
||||||
|
label { font-size:.75rem; color:var(--dim); }
|
||||||
|
label strong { display:block; color:var(--text); font-weight:400; margin-bottom:.25rem; font-size:.8rem; }
|
||||||
|
input[type="file"], select { width:100%; padding:.55rem .7rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; color:var(--text); font-family:inherit; font-size:.8rem; margin-top:.15rem; transition:border-color .2s; }
|
||||||
|
input[type="file"]:hover, select:hover { border-color: var(--accent); }
|
||||||
|
input[type="file"]::file-selector-button { background:var(--border); color:var(--text); border:none; padding:.35rem .8rem; border-radius:4px; margin-right:.5rem; cursor:pointer; font-family:inherit; font-size:.75rem; }
|
||||||
|
select { cursor:pointer; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%235c6e80'%3E%3Cpath d='M6 8L2 4h8z'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right .5rem center; appearance:none; padding-right:2rem; }
|
||||||
|
/* Buttons */
|
||||||
|
.actions { display:flex; gap:.5rem; margin-top:1.25rem; }
|
||||||
|
.btn { padding:.6rem 1.25rem; border-radius:6px; font-family:inherit; font-size:.8rem; cursor:pointer; border:none; transition: all .2s; }
|
||||||
|
.btn-primary { background:var(--accent); color:var(--bg); }
|
||||||
|
.btn-primary:hover { background:#4fc8f0; }
|
||||||
|
.btn-primary:disabled { background:var(--border); color:var(--dim); cursor:not-allowed; }
|
||||||
|
.btn-secondary { background:transparent; color:var(--dim); border:1px solid var(--border); }
|
||||||
|
.btn-secondary:hover { color:var(--text); border-color:var(--dim); }
|
||||||
|
/* Status & Results */
|
||||||
|
#status-area { margin-top:1rem; }
|
||||||
|
.status-card { background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:1rem 1.5rem; font-size:.8rem; }
|
||||||
|
.status-card.success { border-left:3px solid var(--green); }
|
||||||
|
.status-card.pending { border-left:3px solid var(--yellow); }
|
||||||
|
.status-card.error { border-left:3px solid var(--red); }
|
||||||
|
.status-card .title { font-weight:600; margin-bottom:.25rem; }
|
||||||
|
.status-card.success .title { color:var(--green); }
|
||||||
|
.status-card.pending .title { color:var(--yellow); }
|
||||||
|
.status-card.error .title { color:var(--red); }
|
||||||
|
.matrix { display:grid; grid-template-columns:auto 1fr; gap:.3rem 1.5rem; margin:.75rem 0; font-size:.8rem; }
|
||||||
|
.matrix dt { color:var(--dim); }
|
||||||
|
.matrix dd { color:var(--text); }
|
||||||
|
.divider { border-top:1px solid var(--border); margin:1rem 0; }
|
||||||
|
.result-link { display:inline-block; margin-top:.5rem; color:var(--accent); text-decoration:none; font-size:.8rem; }
|
||||||
|
.result-link:hover { text-decoration:underline; }
|
||||||
|
/* Footer */
|
||||||
|
footer { margin-top:3rem; padding-top:1rem; border-top:1px solid var(--border); font-size:.7rem; color:var(--dim); display:flex; justify-content:space-between; }
|
||||||
|
footer a { color:var(--dim); text-decoration:none; }
|
||||||
|
footer a:hover { color:var(--text); }
|
||||||
|
/* Field table */
|
||||||
|
table { width:100%; border-collapse:collapse; font-size:.8rem; }
|
||||||
|
th { text-align:left; color:var(--dim); font-weight:400; padding:.5rem .75rem; border-bottom:1px solid var(--border); }
|
||||||
|
td { padding:.45rem .75rem; border-bottom:1px solid rgba(31,41,55,.5); }
|
||||||
|
tr.pass td:first-child { border-left:3px solid var(--green); }
|
||||||
|
tr.tolerated td:first-child { border-left:3px solid var(--yellow); }
|
||||||
|
tr.fail td:first-child { border-left:3px solid var(--red); }
|
||||||
|
pre { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:1rem; font-family:inherit; font-size:.8rem; overflow-x:auto; margin-top:.5rem; }
|
||||||
|
|||||||
+89
-17
@@ -1,27 +1,99 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html><head><meta charset="utf-8"><title>Result - {{ task.id }}</title>
|
<html lang="en">
|
||||||
<link rel="stylesheet" href="/static/style.css"></head><body>
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Result — {{ task.id }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Verification Result</h1>
|
|
||||||
{% if task.status == "done" and task.result %}
|
<header>
|
||||||
<pre class="pass">Status: {{ task.result.status }}
|
<div class="badge">Verification Result</div>
|
||||||
Program: {{ task.result.program }}
|
<h1>{{ task.id }}</h1>
|
||||||
Matched: {{ task.result.matched }} | Mismatched: {{ task.result.mismatched }}
|
<div class="tagline">Status: <span class="poll-status">loading...</span></div>
|
||||||
Runner: {{ task.result.runner }} | Duration: {{ task.result.duration }}s</pre>
|
</header>
|
||||||
{% elif task.status == "error" %}
|
|
||||||
<pre class="fail">{{ task.result }}</pre>
|
<section>
|
||||||
{% else %}
|
<h2>Summary</h2>
|
||||||
<div id="poll-status">Status: {{ task.status }} — polling...</div>
|
<div class="matrix">
|
||||||
|
<dt>Status</dt><dd>{{ task.result.status }}</dd>
|
||||||
|
<dt>Program</dt><dd>{{ task.result.program }}</dd>
|
||||||
|
<dt>Matched</dt><dd>{{ task.result.matched }}</dd>
|
||||||
|
<dt>Mismatched</dt><dd>{{ task.result.mismatched }}</dd>
|
||||||
|
<dt>Runner</dt><dd>{{ task.result.runner }}</dd>
|
||||||
|
<dt>Duration</dt><dd>{{ task.result.duration }}s</dd>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Field Results</h2>
|
||||||
|
<div id="fields-table"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Pipeline Details</h2>
|
||||||
|
<div id="debug-section"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
<a href="/" class="btn btn-secondary">← New Verification</a>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span>verify-cli v0.2.0</span>
|
||||||
|
<span><a href="#">Docs</a> · <a href="#">Architecture</a></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const id = "{{ task.id }}";
|
const id = "{{ task.id }}";
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const r = await fetch("/status/" + id);
|
const r = await fetch("/status/" + id);
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
document.getElementById("poll-status").textContent = "Status: " + d.status;
|
document.querySelector(".poll-status").textContent = d.status;
|
||||||
if (d.status === "done" || d.status === "error") location.reload();
|
if (d.status === "done" || d.status === "error") location.reload();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
// Load per-field results + debug
|
||||||
|
(async () => {
|
||||||
|
const r = await fetch("/fields/" + id);
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.fields && d.fields.length) {
|
||||||
|
const rows = d.fields.map(f => {
|
||||||
|
const cls = f.status === "PASS" ? "pass" : f.status === "TOLERATED" ? "tolerated" : "fail";
|
||||||
|
return `<tr class="${cls}"><td>${f.name}</td><td>${f.status}</td><td>${f.cobol||""}</td><td>${f.java||""}</td><td>${f.suggestion||""}</td></tr>`;
|
||||||
|
}).join("");
|
||||||
|
document.getElementById("fields-table").innerHTML =
|
||||||
|
`<table><tr><th>Field</th><th>Status</th><th>COBOL</th><th>Java</th><th>Suggestion</th></tr>${rows}</table>`;
|
||||||
|
}
|
||||||
|
// Render debug info
|
||||||
|
const dbg = d.debug || {};
|
||||||
|
let html = "";
|
||||||
|
if (dbg.field_tree) {
|
||||||
|
const treeRows = dbg.field_tree.map(f =>
|
||||||
|
`<tr><td>L${f.level}</td><td>${f.name}</td><td>${f.pic}</td><td>${f.usage}</td><td>${f.offset}</td><td>${f.length}</td></tr>`).join("");
|
||||||
|
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">COPYBOOK FieldTree</h3>
|
||||||
|
<table><tr><th>Lv</th><th>Name</th><th>PIC</th><th>Usage</th><th>Off</th><th>Len</th></tr>${treeRows}</table>`;
|
||||||
|
}
|
||||||
|
if (dbg.test_cases) {
|
||||||
|
const tcList = dbg.test_cases.map(tc =>
|
||||||
|
`<tr><td>${tc.id}</td><td>${JSON.stringify(tc.fields)}</td><td>${(tc.targets||[]).join(", ")}</td></tr>`).join("");
|
||||||
|
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">Test Data (${dbg.test_cases.length} cases)</h3>
|
||||||
|
<table><tr><th>ID</th><th>Fields</th><th>Coverage</th></tr>${tcList}</table>`;
|
||||||
|
}
|
||||||
|
if (dbg.spark_config) {
|
||||||
|
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">Spark Config</h3>
|
||||||
|
<pre>${dbg.spark_config.records} records via key_varied replication</pre>`;
|
||||||
|
}
|
||||||
|
if (dbg.cobol_build) {
|
||||||
|
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">COBOL Compile${dbg.cobol_build.ok?"":" (FAILED)"}</h3>
|
||||||
|
<pre>${dbg.cobol_build.log||"(no output)"}</pre>`;
|
||||||
|
}
|
||||||
|
if (dbg.java_build) {
|
||||||
|
html += `<h3 style="color:var(--dim);font-size:.75rem;letter-spacing:.05em;margin:1rem 0 .5rem">Java Build${dbg.java_build.ok?"":" (FAILED)"}</h3>
|
||||||
|
<pre>${dbg.java_build.log||"(no output)"}</pre>`;
|
||||||
|
}
|
||||||
|
if (html) document.getElementById("debug-section").innerHTML = html;
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
</body>
|
||||||
<a href="/">← New verification</a>
|
</html>
|
||||||
</div>
|
|
||||||
</body></html>
|
|
||||||
|
|||||||
+37
-12
@@ -1,18 +1,43 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html><head><meta charset="utf-8"><title>COBOL→Java Verify</title>
|
<html lang="en">
|
||||||
<link rel="stylesheet" href="/static/style.css"></head><body>
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>COBOL → Java Migration Verification</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>COBOL→Java Migration Verification</h1>
|
|
||||||
|
<header>
|
||||||
|
<div class="badge">Developer Tool</div>
|
||||||
|
<h1><span>verify</span> — COBOL to Java/Spark Migration</h1>
|
||||||
|
<div class="tagline">Automated field-level verification for COBOL→Java migration pipelines</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Upload Sources</h2>
|
||||||
<form id="verify-form" enctype="multipart/form-data">
|
<form id="verify-form" enctype="multipart/form-data">
|
||||||
<label>COPYBOOK:<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label>
|
<div class="form-grid">
|
||||||
<label>COBOL source:<input type="file" name="cobol_src" accept=".cbl" required></label>
|
<label><strong>COPYBOOK</strong>.cpy / .cbl / .copy<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label>
|
||||||
<label>Java source (dir with pom.xml):<input type="file" name="java_src" webkitdirectory required></label>
|
<label><strong>COBOL Source</strong>.cbl<input type="file" name="cobol_src" accept=".cbl" required></label>
|
||||||
<label>Mapping YAML:<input type="file" name="mapping" accept=".yaml,.yml" required></label>
|
<label><strong>Mapping</strong>.yaml / .yml<input type="file" name="mapping" accept=".yaml,.yml" required></label>
|
||||||
<label>Runner:<select name="runner"><option value="native">Native Java</option><option value="spark">Spark Java</option></select></label>
|
<label><strong>Runner</strong><select name="runner"><option value="native">Native Java</option><option value="spark">Spark Java</option></select></label>
|
||||||
<button type="submit">Verify</button>
|
<label class="full"><strong>Java Source</strong>directory with pom.xml<input type="file" name="java_src" webkitdirectory required></label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="btn btn-primary">$ verify</button>
|
||||||
|
<button type="reset" class="btn btn-secondary">Clear</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div id="status"></div>
|
</section>
|
||||||
<div id="result"></div>
|
|
||||||
|
<div id="status-area"></div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<span>verify-cli v0.2.0</span>
|
||||||
|
<span><a href="#">Docs</a> · <a href="#">Architecture</a></span>
|
||||||
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/script.js"></script>
|
<script src="/static/script.js"></script>
|
||||||
</body></html>
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -32,12 +32,20 @@ def main():
|
|||||||
vr = run_pipeline(cfg, data["copybook"], data["cobol_src"],
|
vr = run_pipeline(cfg, data["copybook"], data["cobol_src"],
|
||||||
data["java_src"], data["mapping"])
|
data["java_src"], data["mapping"])
|
||||||
|
|
||||||
|
fields = [{"name":fr.field_name,"status":fr.status,
|
||||||
|
"cobol":fr.cobol_value,"java":fr.java_value,
|
||||||
|
"suggestion":fr.suggestion} for fr in vr.field_results]
|
||||||
|
|
||||||
data["status"] = "done"
|
data["status"] = "done"
|
||||||
|
data["fields"] = fields
|
||||||
|
data["debug"] = vr.debug
|
||||||
data["result"] = {
|
data["result"] = {
|
||||||
"program": vr.program, "status": vr.status,
|
"program": vr.program, "status": vr.status,
|
||||||
"matched": vr.fields_matched, "mismatched": vr.fields_mismatched,
|
"matched": vr.fields_matched, "mismatched": vr.fields_mismatched,
|
||||||
"duration": vr.duration_s, "runner": vr.runner,
|
"duration": vr.duration_s, "runner": vr.runner,
|
||||||
}
|
}
|
||||||
|
if vr.report_path and "BLOCKED" in vr.status:
|
||||||
|
data["build_log"] = vr.report_path
|
||||||
tf.write_text(json.dumps(data))
|
tf.write_text(json.dumps(data))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user