Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_2

This commit is contained in:
2026-10-01 23:30:25 +09:00
7 changed files with 289 additions and 27 deletions
@@ -0,0 +1,38 @@
"""흐름도 적용(PLAN 36-4 · 정함 13) — 마스터 사본 폴더 길 시험.
· 환경 변수 AISLO_MASTER_DIR 가 있으면 M01 저장소 마스터 폴더 = 그 폴더 · 없으면 정본
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PROBE = "from M01_MasterData import M01_MasterData_Store as s; print(s.FOLDER)"
def _folder(env_dir: str | None) -> Path:
env = {k: v for k, v in os.environ.items() if k != "AISLO_MASTER_DIR"}
if env_dir:
env["AISLO_MASTER_DIR"] = env_dir
got = subprocess.run(
[sys.executable, "-c", PROBE],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
timeout=120,
)
assert got.returncode == 0, got.stderr
return Path(got.stdout.strip().splitlines()[-1])
def test_사본_폴더_변수가_있으면_그_폴더(tmp_path: Path) -> None:
assert _folder(str(tmp_path)) == tmp_path.resolve()
def test_변수가_없으면_정본() -> None:
assert _folder(None) == (ROOT / "resources" / "master_data").resolve()
+104
View File
@@ -680,3 +680,107 @@ def test_만나는_상자는_살아_있는_선이_있으면_셈_서버와_같음
k: float(v) for k, v in screen_out.items()
}
assert [[b["kind"], b["level"], b["link"]] for b in srv["breaks"]] == screen_breaks
# ── 36-0b 선 조건 여러 줄(그리고 · 또는) · 값 목록 ─────────────────────────────
def test_조건_여러_줄_그리고_또는_글_식_서버같음() -> None:
lg = _ground()
two = {
"kind": "gate",
"conds": [
{"from": {"box": "in-ground", "name": "지반"}, "op": "=", "value": "토사"},
{"from": {"box": "in-ground", "name": "굴착량"}, "op": "≥", "value": 50},
],
"join": "그리고",
"from": {"box": "in-ground", "name": "지반"},
"op": "=",
"value": "토사",
}
either = {**two, "join": "또는"}
cases = [
(two, {"in-ground": {"지반": "토사", "굴착량": 100}}),
(two, {"in-ground": {"지반": "토사", "굴착량": 10}}),
(either, {"in-ground": {"지반": "암", "굴착량": 100}}),
(either, {"in-ground": {"지반": "암", "굴착량": 10}}),
]
got = _run(
RULE
+ f"const base = {json.dumps(lg, ensure_ascii=False)};\n"
+ f"const cases = {json.dumps(cases, ensure_ascii=False)};\n"
"out(cases.map(([rule, vals]) => {\n"
" const lg = { ...base, links: base.links.map(l => l.id === 'l1' ? { ...l, rule } : l) };\n"
" const ev = E.evalLogic(lg, vals);\n"
" return [R.ruleLabel(rule), R.applyRule(rule, 1, { 지반: vals['in-ground'].지반, 굴착량: vals['in-ground'].굴착량 }).formula,"
" ev.ports['fn-exc'], ev.breaks.filter(b => b.link === 'l1').map(b => b.kind)];\n"
"}));"
)
labels = [g[0] for g in got]
assert labels[0] == "지반 = 토사 그리고 굴착량 ≥ 50 일 때만"
assert labels[2] == "지반 = 토사 또는 굴착량 ≥ 50 일 때만"
assert got[0][1] == 'IF(AND(C1="토사",D1>=50),A1,NA())'
assert got[2][1] == 'IF(OR(C1="토사",D1>=50),A1,NA())'
passed = [g[3] == [] for g in got]
assert passed == [True, False, True, False]
# 서버(Compute) = 화면(Eval)
for (rule, values), (_, _, ports, breaks) in zip(cases, got):
srv_lg = {
**lg,
"links": [{**ln, "rule": rule} if ln["id"] == "l1" else ln for ln in lg["links"]],
}
srv = _server(srv_lg, values)
exc = srv["ports"].get("fn-exc") or {}
assert {k: (None if v is None else float(v)) for k, v in exc.items()} == {
k: (None if v is None else float(v)) for k, v in ports.items()
}
assert [b["kind"] for b in srv["breaks"] if b.get("link") == "l1"] == breaks
def test_옛_한_줄_꼴과_makeGate() -> None:
got = _run(
RULE
+ "const old = { kind: 'gate', from: { box: 'b1', name: '지반' }, op: '=', value: '토사' };\n"
"const made = M.makeGate([{ from: { box: 'b1', name: '지반' }, op: '≠', value: '암' }, { from: { box: 'b1', name: '높이' }, op: '<', value: 3 }], '또는');\n"
"out([M.gateConds(old), R.ruleLabel(old), made.from, made.op, made.value, M.ruleSources(made).map(p => p.name),"
" R.applyRule(made, 7, { 지반: '암', 높이: 2 }).value, R.applyRule(made, 7, { 지반: '암', 높이: 5 }).why]);"
)
conds, label, first_from, first_op, first_value, sources, ok, blocked = got
assert conds == {
"conds": [{"from": {"box": "b1", "name": "지반"}, "op": "=", "value": "토사"}],
"join": "그리고",
}
assert label == "지반 = 토사 일 때만"
assert (first_from, first_op, first_value) == ({"box": "b1", "name": "지반"}, "≠", "암")
assert sources == ["지반", "높이"]
assert (ok, blocked) == (7, "통과 안 됨")
def test_값_목록() -> None:
got = _run(
PIPE
+ "lg = M.updateBox(lg, dia.id, { input: { ...lg.boxes[0].input, variables: [...lg.boxes[0].input.variables,"
" { name: '지반', type: 'choice', unit: '-', choices: ['토사', '암'], example: '토사' }] } });\n"
"lg = M.updateBox(lg, mat.id, { input: { ...lg.boxes[1].input, elements: lg.boxes[1].input.elements.map((e, i) => i === 1 ? { ...e, spec: '12mm' } : e) } });\n"
"r = add(lg, 'input'); lg = r.lg; const tb = r.box;\n"
"lg = M.updateBox(lg, tb.id, { input: { mode: 'group', group: '소요량', elements: [], variables: [], table: { key: 'QF1' } } });\n"
"const vals = { [tb.id]: { 표: [['돌', '석공'], ['깬돌', 0.2], ['견치돌', 0.3], ['깬돌', 0.25]] } };\n"
"const v = (box, name, opts) => M.valueChoices(lg, { box, name }, opts);\n"
"out([v(dia.id, '지반'), v(dia.id, '관경'), v(mat.id, '거푸집 자리'), v(mat.id, '흄관 단가'),"
" v(tb.id, '표', { col: '돌', values: vals }), v(tb.id, '표', { col: '없음', values: vals }), v(tb.id, '표'),"
" v(f.id, '재료값'), v('없는 상자', '지반')]);"
)
assert got == [["토사", "암"], None, ["12mm"], None, ["깬돌", "견치돌"], None, None, None, None]
def test_조건_여러_줄_겹침_안맞음() -> None:
got = _run(
SPLIT
+ "const both = (op1, v1) => M.makeGate([{ from: { box: v.id, name: '지반' }, op: op1, value: v1 }, { from: { box: v.id, name: '높이' }, op: '≥', value: 3 }]);\n"
"let g = M.setRule(lg, lg.links[0].id, both('=', '토사')).lg;\n"
"g = M.setRule(g, lg.links[1].id, both('≠', '암')).lg;\n"
"const over = E.breaks(g, V('토사')).filter(b => b.level === '문제').map(b => b.kind);\n"
"const none = E.breaks(g, V('암', 2)).filter(b => b.level === '문제').map(b => b.kind);\n"
"out([over, none]);"
)
over, none = got
assert "조건 겹침" in over
assert "조건 안 맞음" in none