"""EST 테스트 앱 단가 고르기: 검색·넣기·바꾸기와 계산 연결.""" import json import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] FILES = [ "native-math.js", "native-formula.js", "native-machine.js", "native-rows.js", "native-text.js", "native-quantity.cjs", "native-wonga.js", "native-engine.js", "native-row-match.js", "native-escalation.js", "native-progress.js", "native-business.js", "native-review.js", "native-resources.js", "native-schedule.js", "native-model.js", "native-verify.js", "native-pick.js", ] def run_js(code: str): runner = f""" const fs = require('fs'), vm = require('vm'); const ctx = vm.createContext({{ structuredClone, console }}); for (const f of {json.dumps(FILES)}) vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }}); ctx.project = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8')); process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx))); """ out = subprocess.check_output(["node", "-e", runner], cwd=ROOT) return json.loads(out.decode("utf-8")) def test_search_filters_by_kind_and_all_words(): result = run_js("""(() => { const labor = project.tables.LaborcDat.find((r) => r.Code && r.Name); const word = String(labor.Name).replace(/\\s+/g, "").slice(0, 2); const one = EstPick.search(project, 6, word); const all = EstPick.search(project, 0, labor.Code); const none = EstPick.search(project, 6, word + " 없는말없는말"); return { kinds: [...new Set(one.rows.map((r) => r.kind))], hit: one.rows.some((r) => r.Code === labor.Code), exact: all.rows.filter((r) => r.Code === labor.Code).length, none: none.total, }; })()""") assert result == {"kinds": [6], "hit": True, "exact": 1, "none": 0} def test_insert_links_rows_and_engine_prices_them(): result = run_js("""(() => { const labor = project.tables.LaborcDat.find((r) => r.Code && Number(r.TC) > 0); const pick = EstPick.items(project, 6).find((r) => r.Code === labor.Code); const rows = project.tables.EstMainDat, before = rows.length; const added = EstPick.insert(project, "EstMainDat", 0, "", [pick, pick]); const done = EstNative.calculate(EstModel.validate(project)); const row = done.tables.EstMainDat[added[0]]; return { added, grew: done.tables.EstMainDat.length - before, link: row.LinkIndex, code: row.Code === labor.Code, name: row.Name === labor.Name, priced: Number(row.NU) > 0 && Number(row.NU) === Number(pick.price), }; })()""") assert result == { "added": [1, 2], "grew": 2, "link": 6, "code": True, "name": True, "priced": True, } def test_replace_current_row_or_all_same_code(): result = run_js("""(() => { const rows = project.tables.EstMainDat; const idx = rows.findIndex((r) => r.Code && !(Number(r.Index) > 0) && [5, 6, 103, 104].includes(Number(r.LinkIndex))); const target = rows[idx]; const twin = { ...target, PkID: 999999 }; rows.splice(idx + 1, 0, twin); const pick = EstPick.items(project, 5).find((r) => r.Code !== target.Code); const one = EstPick.replace(structuredClone(project), "EstMainDat", idx, "", pick, false); const copy = structuredClone(project); const all = EstPick.replace(copy, "EstMainDat", idx, "", pick, true); return { one: one.length, all: all.length >= 2, moved: copy.tables.EstMainDat[idx + 1].Code === pick.Code, kind: copy.tables.EstMainDat[idx].LinkIndex, untouched: project.tables.EstMainDat[idx].Code === target.Code, }; })()""") assert result == {"one": 1, "all": True, "moved": True, "kind": 5, "untouched": True} def test_guards_reject_wrong_view_self_reference_and_title_rows(): result = run_js("""(() => { const out = []; const tryIt = (fn) => { try { fn(); out.push("ok"); } catch (e) { out.push(e.message.slice(0, 12)); } }; const head = EstPick.items(project, 103)[0]; tryIt(() => EstPick.insert(project, "CostMainDat", 0, "", [head])); tryIt(() => EstPick.insert(project, "HeadMainDat", 0, head.Code, [head])); tryIt(() => EstPick.insert(project, "EstMainDat", 0, "", [])); const title = project.tables.HeadMainDat.findIndex((r) => Number(r.Index) > 0); tryIt(() => EstPick.replace(project, "HeadMainDat", title, "", EstPick.items(project, 6)[0], false)); return out; })()""") expected = [ "내역서·일위대가 화면", "일위대가 자신을 하위", "넣을 자료를 하나 이상", "일반 내역 행 하나를", ] assert len(result) == len(expected) assert all(message.startswith(head[:8]) for message, head in zip(result, expected)) def test_insert_into_head_group_prices_from_source(): result = run_js("""(() => { const rows = project.tables.HeadMainDat; const title = rows.findIndex((r) => Number(r.Index) > 0); const stuff = EstPick.items(project, 5).find((r) => r.price > 0); const added = EstPick.insert(project, "HeadMainDat", title, rows[title].Code, [stuff]); const row = project.tables.HeadMainDat[added[0]]; return { at: added[0] === title + 1, link: row.LinkIndex, unit: Number(row.GU) === stuff.price }; })()""") assert result == {"at": True, "link": 5, "unit": True}