"""EST 테스트 앱 원가 보조 계산(원본 TfrmWongaDiget 기타경비 계산·산업안전보건관리비 비교).""" 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-wonga-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_anjun_reproduces_original_formula_pair_in_sample(): result = run_js( """(() => { const calc = (p) => EstNative.calculate(EstModel.validate(p)); const p = calc(structuredClone(source)); const plain = (t) => String(t).replace(/\s+/g, ''); const row = p.tables.Wonga.find((r) => plain(r.Name).includes('산업안전') && Number(r.CalcIndex) !== 2); const original = [row.VSusic, p.tables.Wonga.find((r) => plain(r.Name).includes('산업안전') && Number(r.CalcIndex) === 2).VSusic]; const before = p.tables.Wonga.length; const out = EstWongaTools.anjun(p, { youl: 3.15, base: 5349000, addBase: false, compare: true }); const after = calc(p); return { base: EstWongaTools.anjunBase(p.tables.Wonga), original, chosen: out.chosen, other: out.other, rows: p.tables.Wonga.length === before, errors: after.calculation.errors.length }; })()""" ) assert result["base"] == "(A + 4)" assert result["chosen"] == "(A + 4) × 0.0315 × 1.2" assert result["other"] == "((A + 4) + J/1.1) × 0.0315" assert [x.replace(" ", "") for x in result["original"]] == [ result["chosen"].replace(" ", ""), result["other"].replace(" ", ""), ] assert result["rows"] is True and result["errors"] == 0 def test_gita_picks_smaller_base_and_validates(): result = run_js( """(() => { const p = { options: {}, tables: { Wonga: [ { Gubun: 'A', Name: '직 접 공 사 비', Cost: 1000 }, { Gubun: 'B', Name: '직접노무비', Cost: 300 }, { Gubun: 'C', Name: '간접노무비', Cost: 50 }, { Gubun: 'D', Name: '기 타 경 비', Youl: '5%' } ] } }; const out = EstWongaTools.gita(p, { gcRatio: 0.4, baseRatio: 0.6, youl: 5.2, compare: true }); const row = p.tables.Wonga[3], cmp = p.tables.Wonga[4]; const errors = []; for (const bad of [{ gcRatio: 2, baseRatio: 0.5, youl: 1 }, { gcRatio: 0.5, baseRatio: 0.5, youl: 11 }]) try { EstWongaTools.gita(p, bad); } catch (e) { errors.push(e.message.slice(0, 4)); } let none = ''; try { EstWongaTools.anjun({ options: {}, tables: { Wonga: [] } }, { youl: 1 }); } catch (e) { none = e.message; } return { values: out.values, vs: row.VSusic, youl: row.Youl, cmp: [cmp.VSusic, cmp.CalcIndex], errors, none, stored: [p.options.GitaGCRatio, p.options.GitaBaseRatio] }; })()""" ) assert result["values"] == [750, 600] assert result["vs"] == "(A × 0.6) × 0.052" assert result["youl"] == "5.2%" assert result["cmp"] == ["((A × 0.4) + B + C) × 0.052", 2] assert result["errors"] == ["1 이하", "기타경비"] assert result["none"] == "산업안전보건관리비 비목이 없습니다." assert result["stored"] == [0.4, 0.6] def test_diget_round_follows_original_engine(): result = run_js( r"""(() => { const r = (Diget, DigetIndex, v) => EstWonga.digetRound({ Diget, DigetIndex }, v); const row = (Diget, DigetIndex, LinkIndex, v) => EstWonga.rowCost({ Diget, DigetIndex, LinkIndex }, v); return { none: r(3, 0, 1660273), half: r(3, 1, 1660500), cut: r(3, 2, 1660999), up: r(3, 3, 1660001), other: r(2, 7, 1234), neg: r(3, 2, -1660999), dec: r(-2, 1, 1.236), nearly: row(0, 0, 0, 1660273.99996), keep55: row(3, 2, 55, 79603324), cut1: row(1, 2, 0, 1234.9), p: (() => { const p = EstNative.calculate(EstModel.validate(structuredClone(source))); const plain = (t) => String(t).replace(/\s+/g, ''); return p.tables.Wonga.filter((w) => /산업안전|이윤|도급액/.test(plain(w.Name))).map((w) => [plain(w.Name), w.Cost]); })(), }; })()""" ) assert [result[k] for k in ("none", "half", "cut", "up", "other", "neg")] == [ 1660273, 1661000, 1660000, 1661000, 1200, -1660000, ] assert abs(result["dec"] - 1.24) < 1e-9 assert result["nearly"] == 1660274 assert result["keep55"] == 79603324 assert result["cut1"] == 1230 costs = [cost for _, cost in result["p"]] assert {1660273, 1887446, 7620104, 79603000} <= set(costs) def test_position_sets_row_digits_like_original(): result = run_js( r"""(() => { const calc = (p) => EstNative.calculate(EstModel.validate(p)); const p = calc(structuredClone(source)); const plain = (t) => String(t).replace(/\s+/g, ''); const rows = p.tables.Wonga; const anjun = rows.findIndex((r) => plain(r.Name).includes('산업안전') && Number(r.CalcIndex) !== 2); const total = rows.findIndex((r) => Number(r.LinkIndex) === 55); EstWongaTools.position(p, [anjun], 2, 3); const cut = calc(p).tables.Wonga[anjun].Cost; EstWongaTools.position(p, [anjun], 0, 3); const reset = calc(p).tables.Wonga[anjun].Cost; let refused = ''; try { EstWongaTools.position(p, [total], 2, 3); } catch (e) { refused = e.message; } const input = { options: {}, tables: { Wonga: [{ Name: '관급자재대', Cost: 1234567 }] } }; EstWongaTools.position(input, [0], 1, 2); return { cut, reset, refused, input: input.tables.Wonga[0], last: input.options.WongaLastDiget }; })()""" ) assert result["cut"] == 1660000 assert result["reset"] == 1660273 assert result["refused"].startswith("도급액 역산 자리수") assert result["input"]["Cost"] == 1234600 and result["input"]["TC"] == 1234567 assert result["input"]["Diget"] == 2 and result["input"]["DigetIndex"] == 1 assert result["last"] == 2 def test_abbreviate_formula_like_original_sname_show(): result = run_js( r"""(() => { const rows = source.tables.Wonga, a = EstWongaTools.abbreviate(rows, source.options); const by = (g) => a[rows.findIndex((r) => r.Gubun === g)]; const nf = EstWongaTools.abbreviate([{ Index: 1, NumStr: '1', Name: '직 접 노 무 비' }, { NumStr: '2', VSusic: '1 × 0.05', Youl: '5%' }], { VerFormat: false }); return { five: by('5'), c: by('C'), e: by('E'), empty: a[0], nf: nf[1], same: rows.every((r, i) => r.VSusic === source.tables.Wonga[i].VSusic) }; })()""" ) assert result["five"] == "직.노 × 16.5%" assert result["c"] == "( 산출경비 ~ 18 )" assert result["e"] == "순.공 × 8%" assert result["empty"] == "" assert result["nf"] == "직접노무비 × 5%" assert result["same"] is True