perf(M01): 요소·표 저장도 흔들리는 로직만 검사 — 「어느 로직이 무엇을 보는지」 지도 (일감 33)
- 인력 한 줄 저장 7.0초 → 0.9초 · 자재품목 7.2초 → 2.0초 · 소요량 표 1.7초 · 자재품목 저장 뒤 목록 2.5초 → 0.45초 · 켠 뒤 첫 목록 3.4초 → 1.6초 - 로직 줄을 셀 때 `Master.get` 을 덧옷(`_Watch`)으로 감싸 그 줄이 본 자리를 모음 — 키 · 「ID:원문번호」 · 없는 키를 물은 것까지 · 재료 고르기 줄은 자재품목 묶음 통째 - 저장은 그 지도로 흔들리는 줄만 검사(고친 키 · 고치기 전후 원문번호 · 지운 줄) · 요소를 고친 저장은 연결·준용 검사를 그대로 돌림 - 저장 뒤 목록은 흔들린 자리를 알려 받아(`stale(names, marks)`) 그 줄만 다시 셈 · 다른 창이 고친 파일이면 예전처럼 통째로 다시 셈 - 자재품목을 「구분」 으로 미리 갈라 둠(`master_material.by_class`) — 고르기 후보를 3만 줄에서 훑지 않음 · 가른 차례는 파일 차례 그대로라 고르는 줄은 안 바뀜 - 전수 확인 — 요소·표 파일 90 개마다 줄을 통째로 비우고 로직 1,354 줄을 다시 검사, 달라진 줄이 모두 지도 안(지도 밖 0) - 시험 `test_m01_cache.py` 에 셋 더함(열둘) — 쓰이는 요소를 지우면 저장이 막힘 · 요소를 고쳐 저장한 뒤 막힘 표시가 줄마다 검사한 것과 같음 · 인력 줄을 비운 전수 대조 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
@@ -665,7 +665,8 @@ def save(batch: list[dict]) -> list[dict]:
|
||||
for part in batch:
|
||||
_apply(after[part["file"]], part.get("changes") or [], part["file"], book)
|
||||
mk.finish(book)
|
||||
# 고친 줄과 그 줄을 부르던 줄만 검사 — 마스터 전체 검사는 `check_master` 몫
|
||||
# 저장이 흔드는 줄만 검사(고친 줄 · 그 줄을 부르던 줄 · 그 요소를 보던 줄) —
|
||||
# 마스터 전체 검사는 `check_master` 몫
|
||||
narrowed = cache.narrow(FOLDER, batch, before, after)
|
||||
old = set(cache.check_saved(before, names, mk.load_book(book_path), narrowed))
|
||||
new = [x for x in cache.check_saved(after, names, book, narrowed) if x not in old]
|
||||
@@ -683,5 +684,6 @@ def save(batch: list[dict]) -> list[dict]:
|
||||
tmp.write_text(text, encoding="utf-8", newline="\n")
|
||||
os.replace(tmp, FOLDER / name)
|
||||
out.append({"file": name, "version": version_of(text.encode("utf-8"))})
|
||||
cache.stale(dict.fromkeys(names)) # 저장 뒤에는 그 파일을 바로 다시 읽게
|
||||
# 저장 뒤에는 그 파일을 바로 다시 읽게 · 흔들린 자리를 알려 줘 그 줄만 다시 세게
|
||||
cache.stale(dict.fromkeys(names), narrowed[2])
|
||||
return out
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
자국(파일 시각·크기)이 바뀐 파일만 다시 읽고, 로직 막힘도 미리 세어 둠 —
|
||||
목록은 검사를 돌리지 않고 세어 둔 값을 씀. 고치는 길(`calc` · `text` · `save`)은
|
||||
사본을 만지므로 그대로 `cm.load` 로 새로 읽음.
|
||||
저장 전 검사(`check_saved`)도 여기 — 세어 둔 부르기 그림으로 고친 줄 둘레만 봄(`narrow`).
|
||||
저장 전 검사(`check_saved`)도 여기 — 어느 로직이 어느 요소·표·로직을 보는지 세어 두고
|
||||
그것으로 저장이 흔드는 줄만 검사함(`narrow`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,12 +20,14 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_material as mm # noqa: E402 (자재품목 테이블ID — 재료 고르기는 그 묶음을 훑음)
|
||||
|
||||
_FRESH = threading.RLock()
|
||||
_READ: dict[str, tuple[tuple, dict]] = {} # 파일 이름 → (자국, 읽은 내용)
|
||||
_INDEX: dict = {} # 폴더 · 자국 · files · whole
|
||||
_BLOCK: dict[str, tuple[tuple, dict]] = {} # 로직 파일 → (자국, {키: (막힘 까닭, 부르는 로직)})
|
||||
_BLOCK: dict[str, tuple[tuple, dict]] = {} # 로직 파일 → (자국, {키: `_check` 가 센 것})
|
||||
_BASE: dict[str, tuple] = {} # 막힘을 셀 때 바탕이던, 로직 아닌 파일의 자국
|
||||
_MARKED: dict[str, set] = {} # 우리가 고친 요소·표 파일 → 흔들린 자리(`narrow` 가 고른 것)
|
||||
|
||||
|
||||
def _stamps(folder: Path) -> dict[str, tuple]:
|
||||
@@ -43,6 +46,36 @@ def _logic_files(files: dict[str, dict]):
|
||||
yield name, data
|
||||
|
||||
|
||||
class _Watch:
|
||||
"""`Master.get` 을 감싸 그 로직 줄이 본 요소·표를 적어 둠 — 저장 때 검사를 좁히는 지도."""
|
||||
|
||||
def __init__(self, whole):
|
||||
self._whole, self.seen = whole, set()
|
||||
|
||||
def get(self, ref):
|
||||
self.seen.add(str(ref))
|
||||
return self._whole.get(ref)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._whole, name)
|
||||
|
||||
def __setattr__(self, name, value): # 속에 적어 둠 — 미리 갈라 둔 것을 줄마다 다시 안 짜게
|
||||
if name in ("_whole", "seen"):
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
setattr(self._whole, name, value)
|
||||
|
||||
|
||||
def _check(whole, row: dict) -> tuple[list[str], set, set]:
|
||||
"""(막힘 까닭, 부르는 로직, 본 요소·표) — 본 것은 키 · 「ID:원문번호」 ·
|
||||
재료 고르기가 있으면 자재품목 묶음 통째(「#MT」 · 후보를 그 안에서 고름)."""
|
||||
watch = _Watch(whole)
|
||||
why, calls = cm.mf.check_logic(watch, row)
|
||||
if any(isinstance(h.get("요소"), dict) for h in row.get("호표") or []):
|
||||
watch.seen.add("#" + mm.MARKET)
|
||||
return why, calls, watch.seen
|
||||
|
||||
|
||||
def forget() -> None:
|
||||
"""읽어 둔 것을 버림 — 파일을 고친 자리(저장)에서 부름."""
|
||||
with _FRESH:
|
||||
@@ -50,17 +83,19 @@ def forget() -> None:
|
||||
_INDEX.clear()
|
||||
_BLOCK.clear()
|
||||
_BASE.clear()
|
||||
_MARKED.clear()
|
||||
|
||||
|
||||
def stale(names) -> None:
|
||||
def stale(names, marks=()) -> None:
|
||||
"""그 파일만 다시 읽게 함 — 저장처럼 스스로 고친 자리에서 부름
|
||||
(자국은 시각이 같은 틱이면 못 가릴 수 있으므로 고친 쪽이 알려 줌)."""
|
||||
(자국은 시각이 같은 틱이면 못 가릴 수 있으므로 고친 쪽이 알려 줌).
|
||||
`marks` = 요소·표에서 흔들린 자리 — 그 자리를 보던 로직 줄만 다시 셈."""
|
||||
with _FRESH:
|
||||
for name in names:
|
||||
was = _READ.pop(name, (None, {}))[1]
|
||||
_BLOCK.pop(name, None)
|
||||
if was.get("그룹") != "로직":
|
||||
_BASE.clear() # 요소·표가 바뀌면 모든 로직을 다시 셈
|
||||
_MARKED.setdefault(name, set()).update(marks)
|
||||
_INDEX.pop("자국", None)
|
||||
|
||||
|
||||
@@ -90,10 +125,20 @@ def blocks(folder: Path) -> dict[str, list[str]]:
|
||||
files, whole = loaded(folder)
|
||||
stamps = _INDEX["자국"]
|
||||
base = {n: s for n, s in stamps.items() if files[n].get("그룹") != "로직"}
|
||||
marks = None # 요소·표에서 흔들린 자리 — None 이면 무엇이 바뀐지 모름(통째로 다시)
|
||||
if _BASE != base: # 인력 · 재료 · 표가 바뀌면 바탕이 달라짐
|
||||
_BLOCK.clear()
|
||||
off = {n for n in set(_BASE) | set(base) if _BASE.get(n) != base.get(n)}
|
||||
marks = set()
|
||||
for name in off:
|
||||
if name not in _MARKED: # 다른 창이 고친 파일 — 어디가 흔들렸는지 모름
|
||||
marks = None
|
||||
break
|
||||
marks |= _MARKED[name]
|
||||
if marks is None:
|
||||
_BLOCK.clear()
|
||||
_BASE.clear()
|
||||
_BASE.update(base)
|
||||
_MARKED.clear()
|
||||
moved, fresh = set(), set() # 바뀐 로직 파일의 키(옛것 · 새것)
|
||||
names = {name for name, _ in _logic_files(files)}
|
||||
for gone in [name for name in _BLOCK if name not in names]: # 파일째 사라진 것
|
||||
@@ -101,45 +146,64 @@ def blocks(folder: Path) -> dict[str, list[str]]:
|
||||
for name, data in _logic_files(files):
|
||||
if _BLOCK.get(name, (None,))[0] == stamps[name]:
|
||||
continue
|
||||
got = {str(r.get("키")): cm.mf.check_logic(whole, r) for r in data.get("줄") or []}
|
||||
got = {str(r.get("키")): _check(whole, r) for r in data.get("줄") or []}
|
||||
moved |= set(_BLOCK.get(name, (None, {}))[1]) | set(got)
|
||||
_BLOCK[name] = (stamps[name], got)
|
||||
fresh.add(name)
|
||||
for name, data in _logic_files(files): # 다른 파일에서 그 로직을 부르던 줄
|
||||
for name, data in _logic_files(files): # 그 로직을 부르던 줄 · 바뀐 요소를 보던 줄
|
||||
if name in fresh:
|
||||
continue
|
||||
got = _BLOCK[name][1]
|
||||
rows = {str(r.get("키")): r for r in data.get("줄") or []}
|
||||
for key in [k for k, (_, calls) in got.items() if calls & moved]:
|
||||
got[key] = cm.mf.check_logic(whole, rows[key])
|
||||
return {key: why for _, got in _BLOCK.values() for key, (why, _) in got.items()}
|
||||
shaken = [
|
||||
k
|
||||
for k, (_, calls, seen) in got.items()
|
||||
if calls & moved or (marks and seen & marks)
|
||||
]
|
||||
for key in shaken:
|
||||
got[key] = _check(whole, rows[key])
|
||||
return {key: why for _, got in _BLOCK.values() for key, (why, *_) in got.items()}
|
||||
|
||||
|
||||
def graph(folder: Path) -> dict[str, set]:
|
||||
"""로직 키마다 부르는 로직 키 — 미리 세어 둔 것(저장 때 검사를 좁히는 데 씀)."""
|
||||
def counted(folder: Path) -> dict[str, tuple]:
|
||||
"""로직 키마다 (막힘 까닭, 부르는 로직, 본 요소·표) — 미리 세어 둔 것."""
|
||||
with _FRESH:
|
||||
blocks(folder)
|
||||
return {key: calls for _, got in _BLOCK.values() for key, (_, calls) in got.items()}
|
||||
return {key: got for _, rows in _BLOCK.values() for key, got in rows.items()}
|
||||
|
||||
|
||||
def _rows_of(data: dict) -> list:
|
||||
return data.get("표") or data.get("줄") or []
|
||||
|
||||
|
||||
def narrow(folder: Path, batch: list[dict], before: dict, after: dict):
|
||||
"""(검사할 로직 키, 안 보는 줄이 부르는 키) — 저장이 건드린 줄과 그 줄을 부르던 줄만.
|
||||
"""(검사할 로직 키, 안 보는 줄이 부르는 키, 요소도 고쳤는지) — 저장이 흔드는 줄만.
|
||||
|
||||
로직 밖 파일(요소 · 표)이 섞이면 어느 로직이 흔들릴지 모르므로 None — 전부 검사.
|
||||
로직 줄은 고친 줄과 **그 줄을 부르던 줄** · 요소·표 줄은 그 줄을 **보던 로직**
|
||||
(키 · 「ID:원문번호」 · 자재품목은 묶음 통째 — `_Watch` 가 적어 둔 지도).
|
||||
"""
|
||||
if any(after[part["file"]].get("그룹") != "로직" for part in batch):
|
||||
return None
|
||||
calls, touched, gone = graph(folder), set(), set()
|
||||
counts = counted(folder)
|
||||
calls = {key: got[1] for key, got in counts.items()}
|
||||
touched, gone, marks = set(), set(), set()
|
||||
for part in batch:
|
||||
was, now = [
|
||||
{str(r.get("키")) for r in side[part["file"]].get("줄") or []}
|
||||
for side in (before, after)
|
||||
]
|
||||
touched |= now - was # 더한 줄(키는 저장 때 대장이 냄)
|
||||
gone |= was - now
|
||||
touched |= {str(c.get("key")) for c in part.get("changes") or [] if c.get("op") != "add"}
|
||||
name = part["file"]
|
||||
was, now = ({str(r.get("키")): r for r in _rows_of(side[name])} for side in (before, after))
|
||||
keys = (set(now) - set(was)) | {
|
||||
str(c.get("key")) for c in part.get("changes") or [] if c.get("op") != "add"
|
||||
}
|
||||
if after[name].get("그룹") == "로직":
|
||||
touched |= keys
|
||||
gone |= set(was) - set(now)
|
||||
continue
|
||||
for key in keys | (set(was) - set(now)): # 지운 줄도 — 가리키던 자리가 끊김
|
||||
marks |= {key, "#" + key[:2]}
|
||||
for side in (was, now):
|
||||
number = (side.get(key) or {}).get("원문번호")
|
||||
if number:
|
||||
marks.add(f"{key[:2]}:{number}")
|
||||
only = touched | {key for key, called in calls.items() if called & touched}
|
||||
return only, {key: called for key, called in calls.items() if key not in gone}
|
||||
only |= {key for key, got in counts.items() if got[2] & marks}
|
||||
return only, {key: called for key, called in calls.items() if key not in gone}, marks
|
||||
|
||||
|
||||
def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> list[str]:
|
||||
@@ -150,7 +214,10 @@ def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> l
|
||||
"""
|
||||
whole = cm.mf.Master(files)
|
||||
out = [x for name in changed for x in cm.check_form(name, files[name])]
|
||||
return out + cm.check_logics(files, whole, book, narrowed) + cm.mcb.check_all(files, whole)
|
||||
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)
|
||||
|
||||
|
||||
def warm(folder: Path) -> threading.Thread:
|
||||
|
||||
@@ -63,3 +63,24 @@
|
||||
- 저장 뒤에도 잡히는지 — 견본 넷이 모두 422 로 막힘: 없는 로직 키(GC999999) · 없는 인력 요소 키(LB999999) · 돌고 도는 참조 · 고친 줄을 **다른 파일에서** 부르던 줄이 깨짐(입력 하나 더함). 시험 `resources/tester/test_m01_cache.py` 아홉(넷이 이번 것).
|
||||
- 단위 경고(`check_units`)는 애초부터 저장 검사가 아니라 `check_master 단위` 몫 — 14건 그대로.
|
||||
- 남은 늦은 자리 — 저장 0.8초 가운데 0.6초는 마스터를 두 번(고치기 전·후) 새로 읽는 것. **요소 파일 저장은 그대로 7.0초**(인력 한 줄 실측): 요소를 가리키는 로직을 키 글자로만 찾으면 원문번호로 가리키는 자리를 놓쳐 저장 검사에 구멍이 나므로 좁히지 않음 — 좁히려면 「요소를 부르는 로직」 지도부터 만들어야 함.
|
||||
|
||||
## 고친 뒤 3 (일감 33 — 요소 저장 7초)
|
||||
|
||||
요소·표를 한 줄 고쳐도 로직 1,354 줄을 다 검사하던 자리. 잰 자리는 위와 같음.
|
||||
|
||||
| 길 | 앞 | 뒤 |
|
||||
|---|---|---|
|
||||
| 인력 한 줄 저장 | 7.0초 | **0.9초** |
|
||||
| 자재품목 한 줄 저장 | 7.2초 | **2.0초** |
|
||||
| 자재품목 저장 뒤 목록 | 2.5초 | **0.45초** |
|
||||
| 소요량 표 한 줄 저장 | 7초쯤(인력과 같은 길 · 따로 안 쟀음) | **1.7초** |
|
||||
| 로직 한 줄 저장 | 0.8초 | 0.7초 |
|
||||
| 켠 뒤 첫 목록 | 3.4초 | **1.6초**(미리 읽기 1.0초 · 뒤에서 하므로 기다리지 않음) |
|
||||
|
||||
- **어느 로직이 무엇을 보는지 지도** — 로직 줄을 셀 때 `Master.get` 을 덧옷(`_Watch`)으로 감싸 그 줄이 본 자리를 모아 둠(키 · 「ID:원문번호」 · 없는 키를 물은 것까지). 재료 고르기가 있는 줄은 자재품목 묶음 통째(「#MT」) — 후보를 그 안에서 고르므로. 지도는 막힘과 같은 자리에 같은 방식으로 갱신됨(파일 자국이 바뀐 것만).
|
||||
- **저장** — 그 지도로 흔들리는 줄만 검사: 고친 요소의 키 · 원문번호(고치기 전·후 둘 다) · 지운 줄 · 자재품목이면 묶음 통째. 로직 줄 고침은 앞서대로 고친 줄과 그 줄을 부르던 줄. 요소를 고친 저장은 연결·준용 검사(`check_links`)를 그대로 돌림.
|
||||
- **저장 뒤 목록** — 고친 쪽이 흔들린 자리를 알려 주므로(`stale(names, marks)`) 그 자리를 보던 줄만 다시 셈. 다른 창이 고친 파일이면 무엇이 흔들렸는지 모르므로 예전처럼 통째로 다시 셈.
|
||||
- **자재품목을 「구분」 으로 미리 갈라 둠**(`master_material.by_class`) — 고르기 후보를 3만 줄에서 훑던 것을 그 구분 안에서만. 저장·목록·`check_master 로직`(2.1초)·`단위`(1.5초)가 함께 빨라짐. 가른 차례는 파일 차례 그대로라 고르는 줄은 바뀌지 않음.
|
||||
- **전수 확인** — 요소·표 파일 90 개마다 그 파일 줄을 통째로 비운 뒤 로직 1,354 줄을 모두 다시 검사해, 결과가 달라진 줄이 지도가 고른 줄 안에 드는지 봄: **지도 밖 0**(모든 파일에서 「지도 = 달라진 줄」 이 정확히 같음). 지도 한 줄이 보는 자리는 가운데 4 곳 · 가장 많은 170 곳 · 고르기를 쓰는 줄 189.
|
||||
- 시험 — `resources/tester/test_m01_cache.py` 열둘(셋이 이번 것: 쓰이는 요소를 지우면 저장이 막힘 · 요소를 고쳐 저장한 뒤 막힘 표시가 줄마다 검사한 것과 같음 · 인력 줄을 비운 전수 대조가 지도 안에 듦).
|
||||
- 남은 늦은 자리 — 자재품목 저장 2.0초의 대부분은 고르기 쓰는 로직 189 줄을 저장 앞뒤로 두 번 검사하는 것 · 마스터를 두 번 새로 읽는 0.6초.
|
||||
|
||||
@@ -570,7 +570,7 @@ def check_logics(
|
||||
out += [f"{name} · {x}" for x in found]
|
||||
graph[key] = calls
|
||||
out += [f"돌고 도는 참조 · {' → '.join(loop)}" for loop in mf.find_loops(graph)]
|
||||
return out + duplicate_keys(files, book) + ([] if only else check_links(whole))
|
||||
return out + duplicate_keys(files, book) + ([] if only is not None else check_links(whole))
|
||||
|
||||
|
||||
def duplicate_keys(files: dict[str, dict], book: dict | None = None) -> list[str]:
|
||||
|
||||
@@ -116,6 +116,18 @@ def cond_text(cond: dict) -> str:
|
||||
return "|".join(str(cond.get(k) or "") for k in PICK_KEYS)
|
||||
|
||||
|
||||
def by_class(master: mf.Master) -> dict[str, dict]:
|
||||
"""자재품목을 「구분」 으로 미리 갈라 둠(마스터마다 한 번) — 고르기 후보를 찾을 때
|
||||
3만 줄을 다 훑지 않게. 가른 차례는 파일 차례 그대로."""
|
||||
got = getattr(master, "_by_class", None)
|
||||
if got is None:
|
||||
got = {}
|
||||
for key, row in master.index.get(MARKET, {}).items():
|
||||
got.setdefault(row.get("구분"), {})[key] = row
|
||||
master._by_class = got
|
||||
return got
|
||||
|
||||
|
||||
def candidates(
|
||||
master: mf.Master, cond: dict, env: dict | None = None, unit: str | None = None
|
||||
) -> list[str]:
|
||||
@@ -127,9 +139,8 @@ def candidates(
|
||||
need = plain_unit(unit) if unit else ""
|
||||
hits = [
|
||||
key
|
||||
for key, row in master.index.get(MARKET, {}).items()
|
||||
if row.get("구분") == cond.get("구분")
|
||||
and (not detail or row.get("상세구분") == detail)
|
||||
for key, row in by_class(master).get(cond.get("구분"), {}).items()
|
||||
if (not detail or row.get("상세구분") == detail)
|
||||
and all(w in plain_spec(row.get("규격")) for w in want)
|
||||
and (not need or plain_unit(row.get("단위")) == need)
|
||||
]
|
||||
|
||||
@@ -189,3 +189,61 @@ def test_미리_읽어_두면_첫_목록이_기다리지_않는다(client: TestC
|
||||
start = time.perf_counter()
|
||||
assert _logics(client)
|
||||
assert time.perf_counter() - start < 1.0 # 미리 안 읽으면 2~3초
|
||||
|
||||
|
||||
def _plain(file: str) -> dict:
|
||||
"""정본 파일 그대로(수는 글이 아닌 그대로) — 저장에 실어 보낼 줄을 꺼내려고."""
|
||||
return json.loads((store.FOLDER / file).read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _save_file(client: TestClient, file: str, changes: list[dict]):
|
||||
"""정본 파일을 저장 길로 고침 — 요소·표 줄도 같은 길."""
|
||||
return client.post(
|
||||
"/api/m01/save",
|
||||
json={"files": [{"file": file, "version": store.read(file)[1], "changes": changes}]},
|
||||
)
|
||||
|
||||
|
||||
def test_쓰이는_요소를_지우면_저장이_막힌다(client: TestClient) -> None:
|
||||
"""요소 저장도 좁혀 검사하지만 그 요소를 쓰던 로직은 그대로 걸림."""
|
||||
_logics(client) # 미리 세어 둔 값이 선 평소 모양
|
||||
res = _save_file(client, "인력.json", [{"op": "delete", "key": "LB000014"}])
|
||||
assert any("LB000014" in x for x in _errors(res))
|
||||
|
||||
|
||||
def test_요소를_고쳐_저장해도_막힘_표시가_전수와_같다(client: TestClient) -> None:
|
||||
"""요소를 고친 뒤 다시 센 막힘이 줄마다 검사한 것과 같은지 — 좁혀 세는 길."""
|
||||
was = _logics(client)
|
||||
raw = _plain("인력.json")
|
||||
row = {**next(r for r in raw["줄"] if str(r.get("키")) == "LB000014"), "비고": "저장 검사 시험"}
|
||||
res = _save_file(client, "인력.json", [{"op": "edit", "key": "LB000014", "row": row}])
|
||||
assert res.status_code == 200, res.text
|
||||
listed = _logics(client)
|
||||
files, whole = store.loaded()
|
||||
rows = {
|
||||
str(r.get("키")): r for _, d in files.items() if d.get("그룹") == "로직" for r in d["줄"]
|
||||
}
|
||||
assert [x["키"] for x in listed] == [x["키"] for x in was]
|
||||
for one in listed:
|
||||
assert one["reasons"] == store.cm.mf.check_logic(whole, rows[one["키"]])[0]
|
||||
|
||||
|
||||
def test_요소_지도가_전수_검사와_같다(client: TestClient) -> None:
|
||||
"""「어느 로직이 어느 요소를 보는지」 지도가 놓치는 자리가 없는지 —
|
||||
인력 줄을 통째로 비우고 1,354 줄을 다 검사해, 달라진 줄이 지도 안에 드는지 봄."""
|
||||
counts = store.cache.counted(store.FOLDER)
|
||||
files, _ = store.loaded()
|
||||
marks = set()
|
||||
for row in files["인력.json"]["줄"]:
|
||||
key = str(row.get("키"))
|
||||
marks |= {key, "#" + key[:2]}
|
||||
if row.get("원문번호"):
|
||||
marks.add(f"{key[:2]}:{row['원문번호']}")
|
||||
marked = {key for key, got in counts.items() if got[2] & marks}
|
||||
empty = {**files, "인력.json": {**files["인력.json"], "줄": []}}
|
||||
other = store.cm.mf.Master(empty)
|
||||
rows = {
|
||||
str(r.get("키")): r for _, d in files.items() if d.get("그룹") == "로직" for r in d["줄"]
|
||||
}
|
||||
changed = {k for k, r in rows.items() if store.cm.mf.check_logic(other, r)[0] != counts[k][0]}
|
||||
assert changed and changed <= marked
|
||||
|
||||
Reference in New Issue
Block a user