"""EST 테스트 앱 하단으로 집계·공종 다듬기·단가산출 들여쓰기·레벨 확인(원본 f01a90·0x113eb44·0xf0770c·0xd10840).""" 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-est-tools.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.source = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8')); process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx))); """ return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8")) def test_down_sum_inserts_nested_subtotals_and_engine_fills_them(): result = run_js( """(() => { const calc = (p) => EstNative.calculate(EstModel.validate(p)); const p = calc(structuredClone(source)); const total = p.calculation.total, direct = p.calculation.direct.TC; const added = EstEstTools.downSum(p); const again = EstEstTools.downSum(p); const done = calc(p); const rows = done.tables.EstMainDat; const subs = rows.map((r, i) => [r, i]).filter(([r]) => Number(r.LinkIndex) === 53); const first = subs[0][1]; let head = first - 1; while (head >= 0 && !(Number(rows[head].Level) > 0)) head--; const expected = rows.slice(head + 1, first).filter((r) => !Number(r.Level)).reduce((n, r) => n + Number(r.TC || 0), 0); const removed = EstEstTools.upSum(done); return { added: added > 0, again, firstOk: Math.abs(Number(rows[first].TC) - expected) < 1 && expected > 0, name: rows[first].Name, total: done.calculation.total === total, direct: done.calculation.direct.TC === direct, removed: removed === added, errors: done.calculation.errors.length }; })()""" ) assert result == { "added": True, "again": 0, "firstOk": True, "name": "소 계", "total": True, "direct": True, "removed": True, "errors": 0, } def test_left_space_indent_and_level_tree(): result = run_js( """(() => { const p = { options: {}, tables: { EstMainDat: [ { Level: 3, Name: 'A' }, { Level: 2, Name: 'B' }, { Level: 0, Name: 'b1' }, { Level: 2, Name: 'C' }, { Level: 1, Name: 'D' }, { Level: 0, Name: 'd1' }, { Level: 3, Name: 'E' }, { LinkIndex: 54, Level: 0 } ], CostMainDat: [ { Index: 1, Code: 'D1', Name: '터파기', Size: '토사' }, { LineData: ' 가 = \x123\x12' }, { LineData: ' 1', Level: 0 }, { LineData: ' ───── = \x120.5\x12' }, { LineData: ' 2' }, { Level: 2, LineData: ' 큰 공종' }, { SCheck: -1, LineData: '노무 \x1212,345\x12' } ] } }; EstEstTools.leftSpace(p, 2); EstEstTools.costIndent(p, 4); const tree = EstEstTools.levelTree(p, 'D1'); return { left: p.tables.EstMainDat.map((r) => r.LeftSpace), lines: p.tables.CostMainDat.map((r) => r.LineData), tree: tree.title, items: tree.lines.map((l) => l.text.replace(/ {2,}/g, '_')) }; })()""" ) assert result["left"] == [0, 2, 4, 2, 4, 6, 0, 0] assert result["lines"][1] == " 가 = \x123\x12" assert result["lines"][2:5] == [" 1", " ───── = \x120.5\x12", " 2"] assert result["lines"][5] == " 큰 공종" assert result["tree"] == "터파기 (토사)" assert result["items"] == ["큰 공종_[2]", "노무비 : 12,345"]