⚠ **뿌리** — `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>
155 lines
6.1 KiB
Python
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
|