init: cobol-java migration verification platform v3 (42 tests, JCL module)

This commit is contained in:
hangshuo652
2026-05-27 08:42:41 +08:00
parent faeedbc77b
commit 7fcdb41a85
21 changed files with 870 additions and 148 deletions
+54 -45
View File
@@ -1,80 +1,89 @@
"""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 datetime import datetime
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import Config
from orchestrator import run_pipeline
app = FastAPI(title="COBOLJava Verify")
app.mount("/static", StaticFiles(directory=str(Path(__file__).parent / "static")), name="static")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
app = FastAPI(title="COBOL->Java Verify")
BASE = Path(__file__).parent
app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
TASKS_DIR = Path("tasks")
TASKS_DIR.mkdir(exist_ok=True)
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
MAX_SIZE = 10 * 1024 * 1024 # 10MB
TASKS_DIR = Path("tasks"); TASKS_DIR.mkdir(exist_ok=True)
UPLOAD_DIR = Path("uploads"); UPLOAD_DIR.mkdir(exist_ok=True)
MAX_SIZE = 10 * 1024 * 1024
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse("upload.html", {"request": request})
async def index():
return (BASE / "templates" / "upload.html").read_text(encoding="utf-8")
@app.post("/verify")
async def verify(
copybook: UploadFile = File(...),
cobol_src: UploadFile = File(...),
java_src: UploadFile = File(...),
mapping: UploadFile = File(...),
copybook: UploadFile = File(...), cobol_src: UploadFile = File(...),
java_src: UploadFile = File(...), mapping: UploadFile = File(...),
runner: str = Form("native"),
):
task_id = str(uuid.uuid4())[:8]
task_dir = UPLOAD_DIR / task_id
task_dir.mkdir(parents=True, exist_ok=True)
for f, name in [(copybook, "copybook.cpy"), (cobol_src, "program.cbl"),
(java_src, "java"), (mapping, "mapping.yaml")]:
task_dir = UPLOAD_DIR / task_id; task_dir.mkdir(parents=True, exist_ok=True)
for f, name in [(copybook,"copybook.cpy"),(cobol_src,"program.cbl"),
(java_src,"java"),(mapping,"mapping.yaml")]:
content = await f.read()
if len(content) > MAX_SIZE:
raise HTTPException(413, f"{f.filename} exceeds 10MB limit")
dest = task_dir / name
dest.write_bytes(content)
task_file = TASKS_DIR / f"{task_id}.json"
task_file.write_text(json.dumps({
"id": task_id, "status": "queued",
"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)
raise HTTPException(413, f"{f.filename} exceeds 10MB")
(task_dir / name).write_bytes(content)
(TASKS_DIR / f"{task_id}.json").write_text(json.dumps({
"id":task_id,"status":"queued","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)
@app.get("/status/{task_id}")
async def status(task_id: str):
tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists():
raise HTTPException(404, "task not found")
if not tf.exists(): raise HTTPException(404, "task not found")
data = json.loads(tf.read_text())
return JSONResponse({"task_id": task_id, "status": data.get("status", "unknown"),
"result": data.get("result")})
return JSONResponse({"task_id":task_id,"status":data.get("status","unknown"),
"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)
async def result(request: Request, task_id: str):
async def result(task_id: str):
tf = TASKS_DIR / f"{task_id}.json"
if not tf.exists():
raise HTTPException(404, "task not found")
if not tf.exists(): raise HTTPException(404, "task not found")
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
View File
@@ -1,25 +1,36 @@
document.getElementById("verify-form").addEventListener("submit", async (e) => {
e.preventDefault();
const btn = e.target.querySelector("button");
const btn = e.target.querySelector("button[type=submit]");
btn.disabled = true;
btn.textContent = "Submitting...";
document.getElementById("status").textContent = "";
document.getElementById("result").innerHTML = "";
btn.textContent = "$ uploading...";
document.getElementById("status-area").innerHTML = "";
const fd = new FormData(e.target);
try {
const r = await fetch("/verify", { method: "POST", body: fd });
const d = await r.json();
if (r.ok) {
document.getElementById("status").textContent = "Task " + d.task_id + " queued";
document.getElementById("result").innerHTML =
'<p><a href="/result/' + d.task_id + '">View result →</a></p>';
document.getElementById("status-area").innerHTML = `
<div class="status-card pending">
<div class="title">&#9679; 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 &rarr;</a>
</div>`;
} else {
document.getElementById("status").textContent = "Error: " + (d.detail || "unknown");
document.getElementById("status-area").innerHTML = `
<div class="status-card error">
<div class="title">&#10007; Error</div>
${d.detail || "Upload failed"}
</div>`;
}
} catch (err) {
document.getElementById("status").textContent = "Upload failed: " + err.message;
document.getElementById("status-area").innerHTML = `
<div class="status-card error">
<div class="title">&#10007; Network Error</div>
${err.message}
</div>`;
}
btn.disabled = false;
btn.textContent = "Verify";
btn.textContent = "$ verify";
});
+70 -11
View File
@@ -1,11 +1,70 @@
body { font-family: monospace; max-width: 900px; margin: 2rem auto; padding: 0 1rem; color: #333; }
h1 { font-size: 1.2rem; }
label { display: block; margin: .75rem 0; }
input, select { display: block; margin-top: .25rem; }
button { margin-top: 1rem; padding: .5rem 1.5rem; cursor: pointer; font-size: 1rem; }
#status { margin-top: 1rem; font-weight: bold; }
#result { margin-top: 1rem; }
pre { background: #f0f0f0; padding: 1rem; border-radius: 4px; }
.pass { border-left: 4px solid #4caf50; }
.fail { border-left: 4px solid #f44336; }
.container a { display: inline-block; margin-top: 1rem; }
:root {
--bg: #0a0e14;
--panel: #12171f;
--border: #1f2937;
--text: #b2becd;
--dim: #5c6e80;
--accent: #39bae6;
--green: #7fd962;
--red: #f26d78;
--yellow: #ffad66;
--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
View File
@@ -1,27 +1,99 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Result - {{ task.id }}</title>
<link rel="stylesheet" href="/static/style.css"></head><body>
<html lang="en">
<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">
<h1>Verification Result</h1>
{% if task.status == "done" and task.result %}
<pre class="pass">Status: {{ task.result.status }}
Program: {{ task.result.program }}
Matched: {{ task.result.matched }} | Mismatched: {{ task.result.mismatched }}
Runner: {{ task.result.runner }} | Duration: {{ task.result.duration }}s</pre>
{% elif task.status == "error" %}
<pre class="fail">{{ task.result }}</pre>
{% else %}
<div id="poll-status">Status: {{ task.status }} — polling...</div>
<header>
<div class="badge">Verification Result</div>
<h1>{{ task.id }}</h1>
<div class="tagline">Status: <span class="poll-status">loading...</span></div>
</header>
<section>
<h2>Summary</h2>
<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> &middot; <a href="#">Architecture</a></span>
</footer>
</div>
<script>
const id = "{{ task.id }}";
setInterval(async () => {
const r = await fetch("/status/" + id);
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();
}, 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>
{% endif %}
<a href="/">← New verification</a>
</div>
</body></html>
</body>
</html>
+39 -14
View File
@@ -1,18 +1,43 @@
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>COBOL→Java Verify</title>
<link rel="stylesheet" href="/static/style.css"></head><body>
<html lang="en">
<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">
<h1>COBOL→Java Migration Verification</h1>
<form id="verify-form" enctype="multipart/form-data">
<label>COPYBOOK:<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label>
<label>COBOL source:<input type="file" name="cobol_src" accept=".cbl" required></label>
<label>Java source (dir with pom.xml):<input type="file" name="java_src" webkitdirectory required></label>
<label>Mapping YAML:<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>
<button type="submit">Verify</button>
</form>
<div id="status"></div>
<div id="result"></div>
<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">
<div class="form-grid">
<label><strong>COPYBOOK</strong>.cpy / .cbl / .copy<input type="file" name="copybook" accept=".cpy,.cbl,.copy" required></label>
<label><strong>COBOL Source</strong>.cbl<input type="file" name="cobol_src" accept=".cbl" required></label>
<label><strong>Mapping</strong>.yaml / .yml<input type="file" name="mapping" accept=".yaml,.yml" required></label>
<label><strong>Runner</strong><select name="runner"><option value="native">Native Java</option><option value="spark">Spark Java</option></select></label>
<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>
</section>
<div id="status-area"></div>
<footer>
<span>verify-cli v0.2.0</span>
<span><a href="#">Docs</a> &middot; <a href="#">Architecture</a></span>
</footer>
</div>
<script src="/static/script.js"></script>
</body></html>
</body>
</html>
+8
View File
@@ -32,12 +32,20 @@ def main():
vr = run_pipeline(cfg, data["copybook"], data["cobol_src"],
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["fields"] = fields
data["debug"] = vr.debug
data["result"] = {
"program": vr.program, "status": vr.status,
"matched": vr.fields_matched, "mismatched": vr.fields_mismatched,
"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))
except Exception as e: