Files
Aislo/resources/tester/test_spoil_fill_mirror.py
T
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt`
가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는
시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음.

- `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침**
- `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시)
- 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q`
  옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0

⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:12:30 +09:00

155 lines
6.1 KiB
Python

"""사토장(유용토운반작업장) 성토 단면 — 파이썬·TS 짝이 같은 값을 내는지 (2026-09-09).
화면(B06 횡단도)이 그리고 서버(B08 수량)가 세는 값이라 두 쪽이 갈리면
「그림은 이런데 수량은 저렇다」가 된다.
⚠ 폭의 시작점이 **노면 끝**이라는 것과, 폭 상한이 **지반 샘플이 있는 데까지**라는 것을
함께 잠근다. 둘 다 사용자 확정이다.
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from common_util.common_util_spoil_fill import ( # noqa: E402
solve_spoil_width_m,
spoil_fill_section,
spoil_max_width_m,
)
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
#: 왼쪽으로 완만히 내려가는 지반. 노면 끝은 (2.0, 100.0).
GROUND = [(-10.0, 98.0), (0.0, 100.0), (5.0, 99.5), (10.0, 98.0), (20.0, 95.0), (30.0, 92.0)]
_RUNNER = """
import { writeFileSync } from "node:fs";
import { spoilFillSection, solveSpoilWidthM, spoilMaxWidthM } from "./common_util_spoil_fill.js";
const ground = %s.map(([offset_m, elevation_m]) => ({ offset_m, elevation_m }));
const base = { ground, startOffsetM: 2.0, startElevationM: 100.0, side: "left" };
const out = {
maxWidth: spoilMaxWidthM(base),
widths: [0, 2, 4, 8, 40].map((widthM) => {
const s = spoilFillSection({ ...base, widthM, slopeRatioN: 1.5 });
return { area: s.area_m2, toe: s.toeOffsetM, unclosed: s.unclosed, line: s.line.map((p) => [p.offset_m, p.elevation_m]) };
}),
right: (() => {
const s = spoilFillSection({ ...base, side: "right", startOffsetM: -2.0, widthM: 4, slopeRatioN: 1.5 });
return { area: s.area_m2, toe: s.toeOffsetM, line: s.line.map((p) => [p.offset_m, p.elevation_m]) };
})(),
solved: [10, 40, 100000].map((target) => {
const r = solveSpoilWidthM({ ...base, slopeRatioN: 1.5 }, target);
return { widthM: r.widthM, area: r.section.area_m2 };
}),
};
writeFileSync(process.argv[2], JSON.stringify(out));
""" % json.dumps(GROUND)
def _ts_values(tmp_path: Path) -> dict:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(PROJECT_ROOT / "common_util" / "common_util_spoil_fill.ts"),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--moduleResolution",
"bundler",
"--ignoreConfig",
],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
for emitted in out.glob("*.js"):
text = emitted.read_text(encoding="utf-8")
emitted.write_text(
re.sub(r'(from "\./[^"]+?)(")', lambda m: m.group(1) + ".js" + m.group(2), text),
encoding="utf-8",
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "out.json"
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(result)],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
return json.loads(result.read_text(encoding="utf-8"))
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_두_쪽이_같은_값을_낸다(tmp_path: Path) -> None:
ts = _ts_values(tmp_path)
assert ts["maxWidth"] == pytest.approx(spoil_max_width_m(GROUND, 2.0, "left"))
for index, width in enumerate((0, 2, 4, 8, 40)):
py = spoil_fill_section(GROUND, 2.0, 100.0, "left", width, 1.5)
got = ts["widths"][index]
assert got["area"] == pytest.approx(py.area_m2), width
assert got["toe"] == pytest.approx(py.toe_offset_m), width
assert got["unclosed"] == py.unclosed, width
assert [tuple(point) for point in got["line"]] == py.line, width
right = spoil_fill_section(GROUND, -2.0, 100.0, "right", 4, 1.5)
assert ts["right"]["area"] == pytest.approx(right.area_m2)
assert [tuple(point) for point in ts["right"]["line"]] == right.line
for index, target in enumerate((10, 40, 100000)):
width, section = solve_spoil_width_m(GROUND, 2.0, 100.0, "left", 1.5, target)
assert ts["solved"][index]["widthM"] == pytest.approx(width), target
assert ts["solved"][index]["area"] == pytest.approx(section.area_m2), target
def test_폭은_노면_끝에서_잰다() -> None:
"""시작점이 노면 끝이라 **그 구간 노견도 이 성토 안에 든다**(사용자 확정)."""
section = spoil_fill_section(GROUND, 2.0, 100.0, "left", 4.0, 1.5)
assert section.line[0] == (2.0, 100.0)
assert section.line[1] == (6.0, 100.0) # 평상은 노면 끝 높이 그대로
assert section.toe_offset_m > 6.0
def test_넓힐수록_많이_담긴다() -> None:
areas = [spoil_fill_section(GROUND, 2.0, 100.0, "left", w, 1.5).area_m2 for w in (0, 2, 4, 8)]
assert areas == sorted(areas)
assert areas[0] < areas[-1]
def test_지반_샘플_밖으로는_안_넓힌다() -> None:
"""샘플 밖은 지반을 모른다 — 넓히면 근거 없는 부피가 된다."""
limit = spoil_max_width_m(GROUND, 2.0, "left")
assert limit == pytest.approx(28.0)
wide = spoil_fill_section(GROUND, 2.0, 100.0, "left", 100.0, 1.5)
assert wide.toe_offset_m <= 30.0
assert wide.unclosed is True # 지반을 못 만나고 잘렸음을 드러낸다
def test_담을_수_없는_양은_상한_폭을_돌려준다() -> None:
"""임의로 더 넓히지 않는다 — 못 담는 몫은 부르는 쪽이 「남은 사토」로 드러낸다."""
width, section = solve_spoil_width_m(GROUND, 2.0, 100.0, "left", 1.5, 1e6)
assert width == pytest.approx(spoil_max_width_m(GROUND, 2.0, "left"))
assert section.area_m2 < 1e6
def test_기울기가_완만하면_더_담긴다() -> None:
steep = spoil_fill_section(GROUND, 2.0, 100.0, "left", 4.0, 1.0)
gentle = spoil_fill_section(GROUND, 2.0, 100.0, "left", 4.0, 2.0)
assert gentle.area_m2 > steep.area_m2