Phase 2: 轉換管線 — 五個本地轉換器 + convert.py 統一入口 + selfcheck
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
127
tools/convert/selfcheck.py
Normal file
127
tools/convert/selfcheck.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Phase 2 自檢:程式化產生各格式最小測試檔,驗證轉換器與 convert.py 端到端。
|
||||
|
||||
執行:.venv/Scripts/python tools/convert/selfcheck.py
|
||||
邏輯壞掉時 assert 會失敗(AGENTS.md §13.4:一個可執行的檢查,不用框架)。
|
||||
"""
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
||||
import convert
|
||||
import from_docx
|
||||
import from_pdf
|
||||
import from_pptx
|
||||
import from_web
|
||||
import from_xlsx
|
||||
|
||||
|
||||
def make_fixtures(d):
|
||||
import docx as docxlib
|
||||
doc = docxlib.Document()
|
||||
doc.add_heading("支付閘道測試報告", level=1)
|
||||
doc.add_paragraph("逾時缺陷於壓力測試重現。")
|
||||
t = doc.add_table(rows=2, cols=2)
|
||||
t.rows[0].cells[0].text = "案例"
|
||||
t.rows[0].cells[1].text = "結果"
|
||||
t.rows[1].cells[0].text = "TC-001"
|
||||
t.rows[1].cells[1].text = "FAIL"
|
||||
doc.save(d / "report.docx")
|
||||
|
||||
import openpyxl
|
||||
wb = openpyxl.Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "測項"
|
||||
ws.append(["編號", "說明"])
|
||||
ws.append(["TC-001", "逾時 | 重試"])
|
||||
wb.save(d / "cases.xlsx")
|
||||
|
||||
import fitz
|
||||
pdf = fitz.open()
|
||||
page = pdf.new_page()
|
||||
page.insert_text((72, 72), "Payment gateway timeout defect reproduced under load test.")
|
||||
pdf.save(d / "text.pdf")
|
||||
pdf.close()
|
||||
scanned = fitz.open()
|
||||
scanned.new_page() # 無文字層 → 應判 needs_ocr
|
||||
scanned.save(d / "scanned.pdf")
|
||||
scanned.close()
|
||||
|
||||
from pptx import Presentation
|
||||
prs = Presentation()
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
||||
slide.shapes.title.text = "結案報告"
|
||||
slide.notes_slide.notes_text_frame.text = "備註:法遵項目全數通過"
|
||||
prs.save(d / "closing.pptx")
|
||||
|
||||
(d / "page.html").write_text(
|
||||
"<html><body><article><h1>AML 測試指引</h1>"
|
||||
+ "<p>可疑交易監控測試需涵蓋大額交易與分散交易兩種樣態。</p>" * 8
|
||||
+ "</article></body></html>", encoding="utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
tmp = pathlib.Path(tempfile.mkdtemp(prefix="ppqa-selfcheck-"))
|
||||
try:
|
||||
fx = tmp / "fx"
|
||||
fx.mkdir()
|
||||
make_fixtures(fx)
|
||||
|
||||
md, st = from_docx.convert(fx / "report.docx")
|
||||
assert st == "ok" and "# 支付閘道測試報告" in md and "| TC-001 | FAIL |" in md, md
|
||||
md, st = from_xlsx.convert(fx / "cases.xlsx")
|
||||
assert st == "ok" and "## 工作表:測項" in md and "逾時 \\| 重試" in md, md
|
||||
md, st = from_pdf.convert(fx / "text.pdf")
|
||||
assert st == "ok" and "<!-- page 1 -->" in md and "timeout defect" in md, md
|
||||
_, st = from_pdf.convert(fx / "scanned.pdf")
|
||||
assert st == "needs_ocr", st
|
||||
md, st = from_pptx.convert(fx / "closing.pptx")
|
||||
assert st == "ok" and "## Slide 1" in md and "法遵項目全數通過" in md, md
|
||||
md, st = from_web.extract((fx / "page.html").read_text(encoding="utf-8"))
|
||||
assert st == "ok" and "可疑交易監控" in md, md
|
||||
print("PASS: 5 個轉換器 + needs_ocr 偵測")
|
||||
|
||||
# convert.py 端到端(沙盒 root)
|
||||
root = tmp / "root"
|
||||
for sub in ("raw/originals", "raw/converted"):
|
||||
(root / sub).mkdir(parents=True)
|
||||
(root / "raw/manifest.json").write_text('{"version": 1, "files": {}}', encoding="utf-8")
|
||||
|
||||
convert.main([str(fx / "report.docx"), str(fx / "scanned.pdf"),
|
||||
str(fx / "page.html"), "--root", str(root)])
|
||||
m = json.loads((root / "raw/manifest.json").read_text(encoding="utf-8"))
|
||||
origs = {k: v for k, v in m["files"].items() if v["kind"] == "original"}
|
||||
convs = {k: v for k, v in m["files"].items() if v["kind"] == "converted"}
|
||||
assert len(origs) == 3 and len(convs) == 2, m
|
||||
assert origs["raw/originals/scanned.pdf"]["status"] == "needs_ocr"
|
||||
for k, v in convs.items():
|
||||
assert (root / k).is_file(), k
|
||||
assert m["files"][v["original_path"]]["sha256"] == v["original_sha256"]
|
||||
assert (root / "raw/originals/report.docx").is_file()
|
||||
print("PASS: convert.py 端到端 + manifest 對應")
|
||||
|
||||
# 冪等:同檔再跑一次 → skip,manifest 不變
|
||||
before = (root / "raw/manifest.json").read_text(encoding="utf-8")
|
||||
convert.main([str(fx / "report.docx"), "--root", str(root)])
|
||||
assert (root / "raw/manifest.json").read_text(encoding="utf-8") == before
|
||||
print("PASS: 冪等(同 hash 跳過)")
|
||||
|
||||
# dry-run:全新沙盒不落地
|
||||
root2 = tmp / "root2"
|
||||
for sub in ("raw/originals", "raw/converted"):
|
||||
(root2 / sub).mkdir(parents=True)
|
||||
(root2 / "raw/manifest.json").write_text('{"version": 1, "files": {}}', encoding="utf-8")
|
||||
convert.main([str(fx / "report.docx"), "--dry-run", "--root", str(root2)])
|
||||
m2 = json.loads((root2 / "raw/manifest.json").read_text(encoding="utf-8"))
|
||||
assert m2["files"] == {} and not list((root2 / "raw/originals").iterdir())
|
||||
print("PASS: --dry-run 不落地")
|
||||
|
||||
print("ALL PASS")
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user