- 옛 코드 창이 옛 내기계.json 을 다시 써도 폴더 다음 번호가 뒤로 안 감 · 시험 한 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
242 lines
12 KiB
Python
242 lines
12 KiB
Python
"""826-2 기계 = 항목 하나 파일 하나(M01 기계 · 내 기계) + M01 저장 길 줄이기 — 지킴 시험(임시 사본만).
|
|
|
|
① 폴더 → 옛 한 파일 꼴(머리 · 줄 · 키 차례) · 옮기는 빌더 왕복 같음 · 두 번 써도 안 바뀜
|
|
② 한 대 고침 = 파일 하나만 · 새 기계 = 파일 하나 더함(끝) · 지움 = 파일 하나 지움 · 판 충돌 409
|
|
③ 내 기계 — 옛 `내기계.json` 은 읽을 때 폴더로 한 번 옮김(내용 · 다음 번호 같음 · 옛 파일 지움)
|
|
④ 저장 검사 좁히기 — 옛 길(폴더 두 번 읽고 전체 검사 두 번)과 같은 오류 목록(견본 고침 스물)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from M01_MasterData import M01_MasterData_Store as store
|
|
from M01_MasterData import M01_MasterData_Store_Cache as cache
|
|
from M02_MasterTemplete import M02_MyMachine as mym
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
|
|
import master_machine_files as mmf # noqa: E402
|
|
|
|
REAL = store.FOLDER
|
|
cm, mk = store.cm, store.mk
|
|
M, MT, LB, MO = mmf.NAME, "재료_자재품목.json", "인력.json", "재료_유가전력.json"
|
|
|
|
|
|
@pytest.fixture
|
|
def master(tmp_path: Path, monkeypatch) -> Path:
|
|
for path in REAL.glob("*.json"):
|
|
if not path.name.startswith("_") or path.name == mk.BOOK.name:
|
|
shutil.copy(path, tmp_path / path.name)
|
|
shutil.copytree(REAL / mmf.DIR, tmp_path / mmf.DIR)
|
|
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
def _snap(folder: Path) -> dict[str, bytes]:
|
|
return {p.name: p.read_bytes() for p in (folder / mmf.DIR).glob("*.json")}
|
|
|
|
|
|
def _rows(file: str) -> list[dict]:
|
|
data = store.read(file)[0]
|
|
return data[store.items_key(data)]
|
|
|
|
|
|
def _row(file: str, key: str) -> dict:
|
|
return copy.deepcopy(next(r for r in _rows(file) if str(r["키"]) == key))
|
|
|
|
|
|
def _part(file: str, *changes: dict) -> dict:
|
|
return {"file": file, "version": store.read(file)[1], "changes": list(changes)}
|
|
|
|
|
|
# ① 폴더 = 옛 한 파일 꼴 ─────────────────────────────────────────────
|
|
def test_folder_reads_as_one_table(tmp_path: Path) -> None:
|
|
data, version = mmf.table(REAL)
|
|
assert list(data) == ["그룹", "원문", "구분", "줄"] and data["그룹"] == "기계"
|
|
keys = [r["키"] for r in data["줄"]]
|
|
assert len(keys) >= 655 and keys == sorted(keys) == [
|
|
p.stem for p in sorted((REAL / mmf.DIR).glob("EQ*.json"))
|
|
]
|
|
assert not (REAL / M).exists() # 같은 내용 두 벌 없음
|
|
assert cm.load([M])[M] == data and store.read(M) == (data, version)
|
|
assert cm.load()[M] == data # 마스터 전부 읽기에도 같은 이름으로
|
|
# 옮기는 빌더 — 한 파일 → 폴더 → 다시 읽어 같음 · 옛 파일 지움 · 두 번 돌려도 같음
|
|
import importlib
|
|
|
|
build = importlib.import_module("build_기계폴더_826_2")
|
|
(tmp_path / M).write_text(store.dump(data), encoding="utf-8")
|
|
assert build.move(tmp_path) == len(keys) and not (tmp_path / M).exists()
|
|
assert mmf.table(tmp_path) == (data, version) # 같은 글 → 같은 판
|
|
assert build.move(tmp_path) == 0 and mmf.write(tmp_path, data) == []
|
|
|
|
|
|
# ② 고침 · 새 기계 · 지움 · 판 ─────────────────────────────────────────
|
|
def test_one_machine_one_file(master: Path) -> None:
|
|
first = _row(M, "EQ000001")
|
|
before = _snap(master)
|
|
first["값"]["가격"] += 1
|
|
got = store.save([_part(M, {"op": "edit", "key": "EQ000001", "row": first})])
|
|
after = _snap(master)
|
|
assert {n for n in after if after[n] != before.get(n)} == {"EQ000001.json"}
|
|
assert got == [{"file": M, "version": store.read(M)[1]}]
|
|
assert _row(M, "EQ000001") == first and store.loaded()[0][M]["줄"][0] == first
|
|
|
|
new = {**_row(M, "EQ000001"), "이름": "sub1임시기계", "원문번호": ""}
|
|
new.pop("키")
|
|
store.save([_part(M, {"op": "add", "row": new})])
|
|
added = set(_snap(master)) - set(after)
|
|
assert len(added) == 1 and _rows(M)[-1]["이름"] == "sub1임시기계" # 끝(다음 키)
|
|
key = _rows(M)[-1]["키"]
|
|
assert added == {f"{key}.json"} and store.loaded()[1].get(key)["이름"] == "sub1임시기계"
|
|
|
|
stale = _part(M, {"op": "delete", "key": key})
|
|
store.save([_part(M, {"op": "delete", "key": key})])
|
|
assert set(_snap(master)) == set(after) and key not in [r["키"] for r in _rows(M)]
|
|
with pytest.raises(store.StoreError) as e: # 지우기 전 판으로 또 저장
|
|
store.save([stale])
|
|
assert e.value.status == 409 and e.value.detail == {"stale": [M]}
|
|
|
|
|
|
# ③ 내 기계 옮김 ───────────────────────────────────────────────────────
|
|
def test_my_machine_moves_once(tmp_path: Path) -> None:
|
|
rows = [{"키": f"EQP42-{n:04d}", "이름": f"sub1임시{n}", "값": {"가격": n}} for n in (1, 2, 10)]
|
|
old = {"줄": rows, "다음": 11}
|
|
(tmp_path / mym.FILE).write_text(json.dumps(old, ensure_ascii=False), encoding="utf-8")
|
|
got = mym._read(tmp_path)
|
|
assert got == old and not (tmp_path / mym.FILE).exists() # 내용 · 차례 같음 · 옛 파일 지움
|
|
assert sorted(p.name for p in (tmp_path / mym.DIR).iterdir()) == [
|
|
"EQP42-0001.json", "EQP42-0002.json", "EQP42-0010.json", mym.NEXT,
|
|
] # fmt: skip
|
|
assert mym._read(tmp_path) == old and mym.rows(tmp_path) == rows
|
|
assert mym._read(tmp_path / "없음") == {"줄": [], "다음": 1}
|
|
(tmp_path / mym.FILE).write_text(json.dumps({"줄": [], "다음": 5}), encoding="utf-8")
|
|
assert mym._read(tmp_path)["다음"] == 11 # 옛 코드가 다시 쓴 옛 파일 — 번호 뒤로 안 감
|
|
|
|
|
|
# ④ 저장 검사 좁히기 = 옛 길과 같은 오류 ─────────────────────────────────
|
|
def _old_check(files: dict, changed: list[str], book: dict, narrowed) -> list[str]:
|
|
"""826-2 전 `Store_Cache.check_saved` 그대로 — 폴더 전부 색인 · 틀 · 조합 · 모든 층 문서."""
|
|
whole = cm.mf.Master(files)
|
|
out = [x for name in changed for x in cm.check_form(name, files[name])]
|
|
out += cm.check_logics(files, whole, book, narrowed and narrowed[:2])
|
|
if narrowed and narrowed[2]:
|
|
out += cm.check_links(whole)
|
|
return out + cm.mcb.check_all(files, whole) + cache.labor_errors(whole)
|
|
|
|
|
|
def _old(batch: list[dict]) -> list[str]:
|
|
"""826-2 전 저장 검사 길 — 폴더 통째 두 번 읽음 · 전 · 후 전체 검사(쓰지 않음)."""
|
|
before, after = cm.load(folder=store.FOLDER), cm.load(folder=store.FOLDER)
|
|
path = store.FOLDER / mk.BOOK.name
|
|
book = mk.load_book(path)
|
|
for part in batch:
|
|
store._apply(after[part["file"]], copy.deepcopy(part["changes"]), part["file"], book)
|
|
mk.finish(book)
|
|
names = [part["file"] for part in batch]
|
|
narrowed = cache.narrow(store.FOLDER, batch, before, after)
|
|
old = set(_old_check(before, names, mk.load_book(path), narrowed))
|
|
return [x for x in _old_check(after, names, book, narrowed) if x not in old]
|
|
|
|
|
|
def _new(batch: list[dict]) -> list[str]:
|
|
try:
|
|
store.save(copy.deepcopy(batch))
|
|
except store.StoreError as e:
|
|
assert e.status == 422, e.detail
|
|
return e.detail["errors"]
|
|
return []
|
|
|
|
|
|
def _edit(file: str, key: str, fix) -> list[dict]:
|
|
row = _row(file, key)
|
|
fix(row)
|
|
return [_part(file, {"op": "edit", "key": key, "row": row})]
|
|
|
|
|
|
def _delete(file: str, key: str) -> list[dict]:
|
|
return [_part(file, {"op": "delete", "key": key})]
|
|
|
|
|
|
def _add(file: str, like: str, **cols) -> list[dict]:
|
|
row = {**_row(file, like), "원문번호": "", **cols}
|
|
row.pop("키")
|
|
return [_part(file, {"op": "add", "row": row})]
|
|
|
|
|
|
def _logic_with_ref() -> tuple[str, str]:
|
|
for name, data in store._logic_files(store.loaded()[0]):
|
|
for row in data.get("줄") or []:
|
|
for item in row.get("호표") or []:
|
|
if isinstance(item.get("요소"), str) and item["요소"][:2] == "LB":
|
|
return name, str(row["키"])
|
|
raise AssertionError("요소 줄 있는 로직 없음")
|
|
|
|
|
|
def _leaf() -> tuple[str, str]:
|
|
"""아무도 안 부르고 조합에도 없는 로직 — 일위대가 문서만 쓰게 둠."""
|
|
files = store.loaded()[0]
|
|
called = {k for _, calls, _ in cache.counted(store.FOLDER).values() for k in calls}
|
|
held = {k for _, row in cm.mcb.rows(files) for k in cm.mcb.logic_keys(row)}
|
|
for name, data in store._logic_files(files):
|
|
for row in data.get("줄") or []:
|
|
if str(row["키"]) not in called | held:
|
|
return name, str(row["키"])
|
|
raise AssertionError("잎 로직 없음")
|
|
|
|
|
|
def test_narrow_check_same_as_before(master: Path, tmp_path: Path, monkeypatch) -> None:
|
|
leaf_file, leaf = _leaf()
|
|
doc = tmp_path / "labor.json"
|
|
doc.write_text(json.dumps({"담은로직": [{"로직": leaf}]}), encoding="utf-8")
|
|
monkeypatch.setattr(cache, "_labor_files", lambda: [("일위대가 시험", doc)])
|
|
used = next(r["키"] for r in _rows(M) if r.get("원문번호") == "0201-0100")
|
|
ref_file, ref_logic = _logic_with_ref()
|
|
combo = [r["키"] for r in _rows(cm.mcb.FILE)]
|
|
table = next(n for n in store.loaded()[0] if n.startswith("계수_"))
|
|
cell = _rows(table)[0]["키"]
|
|
called = "GC000268" # 317 줄이 부름
|
|
called_file = next(
|
|
n
|
|
for n, d in store._logic_files(store.loaded()[0])
|
|
if any(r["키"] == called for r in d["줄"])
|
|
)
|
|
|
|
def bump(r):
|
|
r["값"]["가격"] += 1
|
|
|
|
cases = [ # (이름, 할 일, 오류가 나야 함)
|
|
("기계 값", lambda: _edit(M, "EQ000001", bump), False),
|
|
("기계 별칭", lambda: _edit(M, "EQ000002", lambda r: r.update(별칭="sub1임시")), False),
|
|
("기계 별칭 빈 글", lambda: _edit(M, "EQ000002", lambda r: r.update(별칭=" ")), True),
|
|
("기계 지움(로직이 씀)", lambda: _delete(M, used), True),
|
|
("기계 원문번호", lambda: _edit(M, used, lambda r: r.update(원문번호="9999-9999")), True),
|
|
("새 기계", lambda: _add(M, "EQ000003", 이름="sub1임시기계"), False),
|
|
("자재 값", lambda: _edit(MT, "MT003175", lambda r: r.update(비고="sub1")), False),
|
|
("자재 지움(로직이 씀)", lambda: _delete(MT, "MT003175"), True),
|
|
("기계 + 자재 이름(고르기 후보 0)", lambda: _edit(M, "EQ000004", bump) + _edit(MT, "MT000008", lambda r: r.update(이름="sub1임시자재")), True),
|
|
("인력 값", lambda: _edit(LB, "LB000003", lambda r: r.update(값=r["값"] + 1)), False),
|
|
("인력 지움(로직이 씀)", lambda: _delete(LB, "LB000002"), True),
|
|
("인력 준용 끊김", lambda: _edit(LB, "LB000006", lambda r: r.update(준용="LB999999")), True),
|
|
("유가 값", lambda: _edit(MO, "MO000001", lambda r: r.update(값=r["값"] + 1)), False),
|
|
("표 조건 틀림", lambda: _edit(table, cell, lambda r: r["조건"].update(sub1="모름")), True),
|
|
("로직 요소 끊김", lambda: _edit(ref_file, ref_logic, lambda r: next(i for i in r["호표"] if str(i.get("요소", ""))[:2] == "LB").update(요소="LB999999")), True),
|
|
("로직 지움(부름 받음)", lambda: _delete(called_file, called), True),
|
|
("로직 지움(일위대가 문서가 씀)", lambda: _delete(leaf_file, leaf), True),
|
|
("조합 이름 겹침", lambda: _edit(cm.mcb.FILE, combo[0], lambda r: r.update(이름=_rows(cm.mcb.FILE)[1]["이름"])), True),
|
|
("조합 지움", lambda: _delete(cm.mcb.FILE, combo[-1]), False),
|
|
("새 자재", lambda: _add(MT, "MT000008", 이름="sub1임시새자재"), False),
|
|
] # fmt: skip
|
|
for name, make, bad in cases:
|
|
batch = make()
|
|
want = _old(batch)
|
|
got = _new(batch)
|
|
assert sorted(got) == sorted(want), name
|
|
assert bool(want) == bad, (name, want[:3])
|