fill.ts가 A의 실제 moveFormula·toA1을 쓰도록 정리(식 채우기 시험 통과). 실무 구조도 xlsx 세 벌 실측 형식 코드 66개 대조 — `_x` trailing 공백을 trim이 삼키던 버그, 글 값에 순수 수 형식이 적용되던 버그, `General"단위"` 접미 버릇, `[$통화-로캘]` 태그 미지원을 고침. 분수 형식(`0/0`)은 1단계 형식 부분집합 밖이라 남겨둠. 회귀 시험 추가. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrQ65VtGZbae2VKgYVMhmc
106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
"""numfmt — 실무 구조도 xlsx 세 벌 실측 숫자 형식 코드 전부를 돌려 안 죽는지 대조(2026-09-27).
|
|
|
|
`resources/knowledge/original/실무문서/` 세 벌(openpyxl 로 읽기만 · 안 고침)에서 쓰인 서로 다른
|
|
숫자 형식 코드를 모아 `spreadsheet_numfmt_survey_entry.ts`(vite ssr → node)로 돌린다.
|
|
값별 정답은 실제 엑셀이 있어야 재는데(이 환경엔 없음) `resources/tester/spreadsheet/
|
|
spreadsheet_b_test_entry.ts` 에 실측 코드 몇 개를 손으로 대조한 회귀 시험을 이미 넣었다 —
|
|
여기는 "실무에 쓰인 코드 전부가 죽지 않고 뭔가 보이는 글을 냄"만 잰다(폭넓은 그물).
|
|
|
|
알려진 빈 자리(1단계 밖 · `9_엑셀검토.md` 형식 부분집합에 없음):
|
|
- 분수 형식(`0/0`, `# ?/?` 류) — 미지원 · 자리표시만 있고 값은 틀리게 나옴.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import openpyxl
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
ENTRY = ROOT / "resources" / "tester" / "spreadsheet" / "spreadsheet_numfmt_survey_entry.ts"
|
|
VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"
|
|
|
|
FILES = [
|
|
ROOT
|
|
/ "resources/knowledge/original/실무문서/25년 산불진화임도(기번8)(울진소광)/1. 설계원본/02-수량/07-구조도-소광리.xlsx",
|
|
ROOT
|
|
/ "resources/knowledge/original/실무문서/2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/02 산출자료/5. 구조도(기번3).xlsx",
|
|
ROOT
|
|
/ "resources/knowledge/original/실무문서/20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/02.엑셀자료/구조도/구조도(변경).xlsx",
|
|
]
|
|
|
|
# 미지원으로 이미 아는 코드(1단계 밖 · 분수 형식) — 죽지 않아도 값이 틀리므로 미리 뺌.
|
|
KNOWN_UNSUPPORTED = {"0/0"}
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not all(f.is_file() for f in FILES) or shutil.which("node") is None,
|
|
reason="실무 xlsx 세 벌 또는 node 가 없음",
|
|
)
|
|
|
|
|
|
def _collect_codes() -> list[dict]:
|
|
codes: dict[str, dict] = {}
|
|
for f in FILES:
|
|
wb = openpyxl.load_workbook(f, data_only=True, read_only=True)
|
|
for ws in wb.worksheets:
|
|
for row in ws.iter_rows():
|
|
for cell in row:
|
|
if cell.value is None or isinstance(cell.value, bool):
|
|
continue
|
|
fmt = cell.number_format
|
|
if fmt == "General":
|
|
continue
|
|
info = codes.setdefault(fmt, {"count": 0, "num": None, "str": None})
|
|
info["count"] += 1
|
|
if isinstance(cell.value, (int, float)) and info["num"] is None:
|
|
info["num"] = cell.value
|
|
if isinstance(cell.value, str) and info["str"] is None:
|
|
info["str"] = cell.value
|
|
wb.close()
|
|
return [{"code": k, **v} for k, v in codes.items()]
|
|
|
|
|
|
def _run_survey(samples: list[dict]) -> list[dict]:
|
|
with tempfile.TemporaryDirectory(prefix="numfmt_survey_") as outdir:
|
|
entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/")
|
|
build = subprocess.run(
|
|
["node", str(VITE), "build", "--configLoader", "runner", "--ssr", entry_rel, "--outDir", outdir],
|
|
cwd=str(ROOT),
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=120,
|
|
)
|
|
assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}"
|
|
bundle = Path(outdir) / "spreadsheet_numfmt_survey_entry.js"
|
|
input_path = Path(outdir) / "codes.json"
|
|
input_path.write_text(json.dumps(samples, ensure_ascii=False), encoding="utf-8")
|
|
run = subprocess.run(
|
|
["node", str(bundle), str(input_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
errors="replace",
|
|
timeout=60,
|
|
)
|
|
assert run.stdout.strip(), f"시험 진입이 아무 결과도 안 냄:\n{run.stderr}"
|
|
return json.loads(run.stdout)
|
|
|
|
|
|
def test_real_world_format_codes_do_not_error():
|
|
samples = _collect_codes()
|
|
assert len(samples) > 30, f"실무 코드가 너무 적게 걸림: {len(samples)}"
|
|
results = _run_survey(samples)
|
|
broken = [
|
|
r
|
|
for r in results
|
|
if r["code"] not in KNOWN_UNSUPPORTED and (r["numErr"] or r["strErr"])
|
|
]
|
|
assert broken == [], json.dumps(broken, ensure_ascii=False, indent=1)
|