sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Smoke / E2E test for application-form PDF export (docxtpl + LibreOffice).
|
||||
|
||||
Direct (no HTTP): exercises merge + conversion on this machine / container.
|
||||
cd be0 && python tools/e2e_application_form_pdf_export.py --direct --out /tmp/e2e-mau.pdf
|
||||
|
||||
HTTP: needs FastAPI listening (--url is the API origin, no /api prefix in path).
|
||||
python tools/e2e_application_form_pdf_export.py --http http://127.0.0.1:4402 --out /tmp/e2e-mau.pdf
|
||||
|
||||
Pure curl + jq (no Python on host; official JSON must be wrapped):
|
||||
cd /path/to/repo && jq -n --slurpfile o fe0/public/assets/bieu_mau_sang_kien_template.json \
|
||||
'{officialBieuMau: $o[0]}' | curl -sfS -X POST http://127.0.0.1:4402/api/v1/docx/preview-application-form-pdf \
|
||||
-H 'Content-Type: application/json' -o /tmp/e2e-mau.pdf && file /tmp/e2e-mau.pdf
|
||||
|
||||
If the API returns HTTP 501, rebuild/run be0 with LibreOffice (see be0/Dockerfile: libreoffice-writer-nogui).
|
||||
|
||||
Docker (stack up):
|
||||
docker compose exec be0 python tools/e2e_application_form_pdf_export.py --direct --out /tmp/e2e-mau.pdf
|
||||
|
||||
Refresh bundled sample after template changes:
|
||||
cp ../fe0/public/assets/bieu_mau_sang_kien_template.json tools/e2e_sample_official_bieu_mau.json
|
||||
|
||||
Exit 0 on success; non-zero on failure.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
BE0_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(BE0_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(BE0_ROOT))
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return BE0_ROOT.parent
|
||||
|
||||
|
||||
def load_sample_official() -> Dict[str, Any]:
|
||||
"""officialBieuMau-shaped JSON (same as Review « Xem lại »)."""
|
||||
candidates = [
|
||||
Path(__file__).resolve().parent / "e2e_sample_official_bieu_mau.json",
|
||||
_repo_root() / "fe0/public/assets/bieu_mau_sang_kien_template.json",
|
||||
]
|
||||
for p in candidates:
|
||||
if p.is_file():
|
||||
with open(p, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
"No sample official JSON found. Expected tools/e2e_sample_official_bieu_mau.json "
|
||||
"or fe0/public/assets/bieu_mau_sang_kien_template.json next to be0/."
|
||||
)
|
||||
|
||||
# Visible markers in generated PDF/DOCX
|
||||
if isinstance(data.get("TRANG BÌA"), dict):
|
||||
data["TRANG BÌA"]["Tên sáng kiến (Tiếng Việt)"] = "E2E PDF export — tên sáng kiến kiểm thử"
|
||||
data["TRANG BÌA"]["Năm"] = "2026"
|
||||
return data
|
||||
|
||||
|
||||
def run_direct(out_path: Path | None) -> bytes:
|
||||
from src.be01.docx_to_pdf import convert_docx_bytes_to_pdf
|
||||
from src.be01.fill_application_form import fill_application_form_docx
|
||||
from src.be01.official_to_data_blank import official_to_data_blank
|
||||
|
||||
official = load_sample_official()
|
||||
ctx = official_to_data_blank(official)
|
||||
docx = fill_application_form_docx(ctx)
|
||||
pdf = convert_docx_bytes_to_pdf(docx)
|
||||
if not pdf.startswith(b"%PDF"):
|
||||
raise RuntimeError("Output is not a PDF (missing %PDF header).")
|
||||
if out_path:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(pdf)
|
||||
print(f"Wrote {len(pdf)} bytes → {out_path}")
|
||||
else:
|
||||
print(f"OK: generated PDF, {len(pdf)} bytes (no --out)")
|
||||
return pdf
|
||||
|
||||
|
||||
def run_http(base_url: str, out_path: Path | None) -> bytes:
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
official = load_sample_official()
|
||||
url = base_url.rstrip("/") + "/api/v1/docx/preview-application-form-pdf"
|
||||
body = json.dumps({"officialBieuMau": official}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json", "Accept": "application/pdf"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||
raw = resp.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
detail = e.read().decode("utf-8", errors="replace")
|
||||
raise SystemExit(f"HTTP {e.code}: {detail}") from e
|
||||
|
||||
if not raw.startswith(b"%PDF"):
|
||||
raise SystemExit(f"Expected PDF, got {len(raw)} bytes, head={raw[:64]!r}")
|
||||
if out_path:
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(raw)
|
||||
print(f"Wrote {len(raw)} bytes → {out_path}")
|
||||
else:
|
||||
print(f"OK: HTTP PDF, {len(raw)} bytes")
|
||||
return raw
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
g = ap.add_mutually_exclusive_group(required=True)
|
||||
g.add_argument("--direct", action="store_true", help="Run docxtpl + LibreOffice in-process")
|
||||
g.add_argument("--http", metavar="BASE_URL", help="POST to FastAPI (e.g. http://127.0.0.1:4402)")
|
||||
ap.add_argument("--out", type=Path, metavar="FILE.pdf", help="Write PDF to this path")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
if args.direct:
|
||||
run_direct(args.out)
|
||||
else:
|
||||
run_http(args.http, args.out)
|
||||
except FileNotFoundError as e:
|
||||
print(f"FAIL: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
except Exception as e:
|
||||
print(f"FAIL: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user